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"
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.