I have an ExpressJS server that is sometimes rendering the wrong user's data on initial render. See a (slightly simplified) version below.
The issue is that the index.ejs
file is often rendering the wrong user's data in reduxState
...
My confusion is because I would expect the call to import { store } from 'routes.js'
to overwrite the store as {}
with every individual user's request. The problem seems to be that the store
is becoming a combined store for every user on the site put together.
How can I make sure that each user sees only their data on the site?
routes.js
// src/routes.js
import React from 'react';
import { createStore, applyMiddleware, compose } from "redux";
import routerConfig from "base/routes/routes";
import thunk from "redux-thunk";
import { rootReducer } from "base/reducers";
let initialState = {};
const store = createStore(
rootReducer, initialState, compose(applyMiddleware(thunk))
);
const routes = routerConfig(store);
export {store};
export default routes;
server.js
import { store } from 'routes';
let getReduxPromise = (renderProps, request) => {
let store = require('./routes/index.jsx').store
let { query, params } = renderProps
let comp = renderProps.components[renderProps.components.length - 1];
let at = null;
if (request && request.cookies && request.cookies.accessToken) {
at = request.cookies.accessToken
}
if (comp.fetchData) {
return comp.fetchData({ query, params, store, at }).then(response => {
if (request) {
if (request.cookies && request.cookies.accessToken && request.cookies.userInfo) {
store.dispatch(
actions.auth(request.cookies.userInfo),
request.cookies.accessToken
)
} else {
store.dispatch(actions.logout())
}
}
return Promise.resolve({response, state: store.getState()})
});
} else {
return Promise.resolve();
}
}
app.get('*', (request, response) => {
let htmlFilePath = path.resolve('build/index.html' );
// let htmlFilePath = path.join(__dirname, '/build', 'index.html');
let error = () => response.status(404).send('404 - Page not found');
fs.readFile(htmlFilePath, 'utf8', (err, htmlData) => {
if (err) {
console.log('error 1')
error();
} else {
match({routes, location: request.url}, (err, redirect, renderProps) => {
if (err) {
console.log('error 2')
error();
} else if (redirect) {
return response.redirect(302, redirect.pathname + redirect.search)
} else if (renderProps) {
let parseUrl = request.url.split('/');
if (request.url.startsWith('/')) {
parseUrl = request.url.replace('/', '').split('/');
}
// User has a cookie, use this to help figure out where to send them.
if (request.cookies.userInfo) {
const userInfo = request.cookies.userInfo
if (parseUrl[0] && parseUrl[0] === 'profile' && userInfo) {
// Redirect the user to their proper profile.
if (renderProps.params['id'].toString() !== userInfo.id.toString()) {
parseUrl[1] = userInfo.id.toString();
const url = '/' + parseUrl.join('/');
return response.redirect(url);
}
}
}
getReduxPromise(renderProps, request).then((initialData) => {
let generatedContent = initialData.response ? render(request, renderProps, initialData.response) : render(request, renderProps, {});
const title = initialData.response.seo.title || '';
const description = initialData.response.seo.description || '';
var draft = [];
const currentState = initialData.state;
if (currentState) {
const reduxState = JSON.stringify(currentState, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (draft.indexOf(value) !== -1) {
// Circular reference found, discard key
return;
}
// Store value in our collection
draft.push(value);
}
return value;
});
draft = null;
ejs.renderFile(
path.resolve('./src/index.ejs' ),
{
jsFile,
cssFile,
production,
generatedContent,
reduxState,
title,
description
}, {},
function(err, str) {
if (err) {
console.log('error 3')
console.log(err);
}
response.status(200).send(str);
});
} else {
console.log('error 4')
console.log(err)
error();
}
}).catch(err => {
console.log('error 5')
console.log(err)
error();
});
} else {
console.log('error 6')
console.log(err)
error();
}
});
}
})
});
index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<title><%- title %></title>
<meta name="description" content="<%- description %>"/>
<link href="<%- cssFile %>" rel="stylesheet"/>
<script type="text/javascript" charset="utf-8">
window.__REDUX_STATE__ = <%- reduxState %>;
</script>
</head>
<body>
<div id="root"><%- generatedContent %></div>
<script type="text/javascript" src="<%- jsFile %>" defer></script>
</body>
</html>
example fetchData
function in a React component
ExamplePage.fetchData = function (options) {
const { store, params, at } = options
return Promise.all([
store.dispatch(exampleAction(params.id, ACTION_TYPE, userAccessToken))
]).spread(() => {
let data = {
seo: {
title: 'SEO Title',
description: 'SEO Description'
}
}
return Promise.resolve(data)
})
}
Variables defined in module scope have only one copy in the entire runtime environment. This means each node.js process has its own copy, and each browser tab/frame has its own copy. However, inside each tab or each process, there is only one copy. This means you cannot define your store as a module-level const and still have a new store for each user. You can solve it like this:
src/routes.js
server.js
This creates a new store for each request and allows each user to have their own data. Note that if you need the same store from different modules, you'll need to pass it around. You can't just
import { newUserEnv }
because it will create a new one.