Server rendered React ExpressJS frontend leaking users' Redux store data

252 views Asked by At

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)
  })
}
1

There are 1 answers

0
DDS On BEST ANSWER

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

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 = {};

export function newUserEnv() {
  const store = createStore(
    rootReducer, initialState, compose(applyMiddleware(thunk))
  );

  const routes = routerConfig(store);
  return { store, routes };
}

server.js

import { newUserEnv } from 'routes';

let getReduxPromise = (renderProps, request) => {
  const { store } = newUserEnv();
  let { query, params } = renderProps
...

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.