Search code examples
node.jsreactjsexpressreact-routerserver-side-rendering

react-router-dom v6 StaticRouter context is not working


I'm trying to make SSR React web application. Everything works fine except staticContext.

My server code is

// IMPORTS

const renderer = (req, store, context) => {
    const content = renderToString(
        <Provider store={store}>
            <ThemeProvider theme={responsiveFontSizes(theme)}>
                <CssBaseline />
                <StaticRouter location={req.path} context={context}>
                    <Navigation />
                    <main>
                        {renderRoutes(MainRouter)}
                    </main>
                    <Footer />
                </StaticRouter>
            </ThemeProvider>
        </Provider>
    );

    const helmet = Helmet.renderStatic();

    return `
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8" />
                <link rel="icon" href="${process.env.REACT_APP_URL}/favicon.ico" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <meta name="theme-color" content="${process.env.REACT_APP_THEME_COLOR}" />
                ${helmet.title.toString()}
                ${helmet.meta.toString()}
                <link rel="stylesheet" href="main.css">
                <link rel="preconnect" href="https://fonts.googleapis.com">
                <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
                <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
            </head>
            <body>
                <noscript>Pre dokonalé fungovanie tejto webovej aplikácie povoľte JavaScript.</noscript>
                <div id="root">${content}</div>
                <script>
                    window.INITIAL_STATE = ${serialize(store.getState())}
                </script>
                <script src="bundle.js"></script>
            </body>
        </html>
    `;
};

app.get('*', (req, res) => {

    const store = createStore();

    var promises = matchRoutes(MainRouter, req.path).map(route => {
    return route?.loadData ? route.loadData(store) : [];
    });

    promises.push(loadData(store, req.cookies));

    Promise.all(promises).then(() => {
        const context = {};
        const content = renderer(req, store, context);

        if(context.url) {
            return res.redirect(302, context.url);
        }

        if(context.notFound) {
            console.log('404');
            res.status(404);
        };

        res.send(content);
    });
});

And my client code of 404 component is

const NotFound = ({ staticContext = {} }) => {
    staticContext.notFound = true;

    const head = () => (
        <Helmet>
            <title>{process.env.REACT_APP_NAME} – Stránka sa nenašla</title>
        </Helmet>
    );

    return (
        <React.Fragment>
            {head()}
            <section>
                {/* Some content */}
            </section>
        </React.Fragment>
    )
};

export default {
    Component: NotFound
};

This line should pass notFound = true argument into the request:

staticContext.notFound = true;


And so this part of code should set request status to 404 and console log '404' string:

if(context.notFound) {
    console.log('404');
    res.status(404);
};

But it doesn't work. Is it possible that my code is deprecated? I'm using currently latest stable versions of all dependencies.

My package.json dependencies

"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.2.5",
"@mui/material": "^5.2.5",
"axios": "0.24.0",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-loader": "8.2.3",
"compression": "1.7.4",
"concurrently": "6.5.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"date-fns": "^2.27.0",
"express": "4.17.2",
"lodash": "4.17.21",
"nodemon": "2.0.15",
"npm-run-all": "4.1.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "^7.2.6",
"react-router-dom": "6.2.1",
"react-spinners": "^0.11.0",
"redux": "^4.1.2",
"serialize-javascript": "6.0.0",
"universal-cookie": "^4.0.4",
"webpack": "5.65.0",
"webpack-dev-server": "4.7.1",
"webpack-merge": "5.8.0",
"webpack-node-externals": "3.0.0"

Solution

  • Is it possible that my code is deprecated?

    I'm afraid it is.

    Here is one of the major changes of v6.0.0-alpha.4:

    Removed the <StaticRouter context> API. We don't support navigation on the initial render in v6, so this API is unnecessary.

    I ended up storing the status code and the redirect URL in the Redux store and checking them on the render server after rendering.

    For the status code I dispatch the setStatus(404) action in the loadData function of the NotFound page component.

    And this is my solution for the redirect URL:

    // router.js
    
    import {useRoutes} from 'react-router-dom';
    
    import getRoutes from '@client/router/routes';
    import {useAuthSelector} from '@client/store/slices/auth';
    
    export default ({ssr = false}) => {
      const {currentUser} = useAuthSelector();
    
      return useRoutes(getRoutes(currentUser, ssr));
    };
    
    // routes.js
    
    import requireAuth from './requireAuth';
    import App from '@client/App';
    import Home from '@client/pages/Home';
    import Users from '@client/pages/Users';
    import Admins from '@client/pages/Admins';
    import NotFound from '@client/pages/NotFound';
    
    export default (currentUser = false, ssr = false) => {
      const protectedRoute = requireAuth(currentUser, ssr);
    
      return [
        {
          ...App,
          children: [
            {
              path: '/',
              ...Home,
            },
            {
              path: '/users',
              ...Users,
            },
            {
              path: '/admins',
              ...protectedRoute(Admins),
            },
            {path: '*', ...NotFound},
          ],
        },
      ];
    };
    
    // requireAuth.js
    
    import React from 'react';
    import {Navigate} from 'react-router-dom';
    
    import {setRedirectUrl} from '@client/store/slices/http';
    
    export default (currentUser, ssr = false) => (component) => {
      switch (currentUser) {
        case false:
          if (ssr) {
            // If the user is not logged in and the SSR is happening
            const loadData = component.loadData;
    
            component.loadData = (store) => {
              // Call the original loadData if component has one
              loadData && loadData(store);
              store.dispatch(setRedirectUrl('/'));
            };
          } else {
            // If the user is not logged in
            component.element = <Navigate to='/' replace />;
          }
          break;
    
        case null:
          component.element = <div>Loading...</div>;
          break;
    
        default:
          return component;
      }
    
      return component;
    };
    

    When I use my router on the render server, I just set the ssr prop to true.

    Another approach that comes to mind is to use a React context instead of the Redux store.