React SSR blinks page - javascript

I created a project with React, react-router, #loadable/component.
Now I'm trying to add SSR to this project.
I did server side rendering with react-router.
And then I added #loadable/component to import all pages component:
import loadable from '#loadable/component';
const routersConfig = [
{
path: '/',
component: loadable(() => import('./Home')),
exact: true,
},
{
path: '/2',
component: loadable(() => import('./Home2')),
exact: true,
},
];
Then I added all this parts of code: https://www.smooth-code.com/open-source/loadable-components/docs/server-side-rendering/
And now it works.
But It works with the problem: a content blinks while loading.
How I understand the page's loading process:
Browser gets a content generated by SSR (the first query in network tab)
Browser renders a content (with left and top margins )
Browser downloads two enterpoints and vendors from html (app.js, normalizer.js, vendor.js)
Browser executes app.js and normalizer.js. Left and top margins are removed.
App.js starts downloading page's chunk - home.js. In this moment content disappears
When home.js is downloaded, the content appears again.
I shoot a video to illustrate this process. (I'm sorry for quality, stackoverflow forbides files which size is more then 2MB ). I'm throttling network speed to imagine all page's download process.
My question is why the content disappears? How to fix it?
My code
server.js
const sheetStyledComponents = new ServerStyleSheet();
const sheetsJssRegistry = createSheetsRegistry();
const statsFile = path.resolve(process.cwd(), './build-ssr/dist/loadable-stats.json');
const extractor = new ChunkExtractor({
statsFile,
entrypoints: [
'app',
'normalize',
],
});
try {
const client = ApolloSSRClient();
const tree = (
<ApolloProvider client={client}>
<ApplyTheme sheetsRegistry={sheetsJssRegistry}>
<StaticRouter location={req.url}>
<Home />
</StaticRouter>
</ApplyTheme>
</ApolloProvider>
);
// there is combination of Apollo graphql, jss, styledComponent functions
const body = await getMarkupFromTree({
renderFunction: flow(
sheetStyledComponents.collectStyles.bind(sheetStyledComponents),
extractor.collectChunks.bind(extractor),
renderToString
),
tree,
});
const scriptTags = extractor.getScriptTags();
// It isn't used yet
const linkTags = extractor.getLinkTags();
const styleTags = sheetStyledComponents.getStyleTags();
const html = (await rawHtml)
.replace(
'</head>',
`
${styleTags}
<style type="text/css" id='jss-server-side-styles'>
${sheetsJssRegistry.toString()}
</style>
<script>
window.__APOLLO_STATE__ = ${JSON.stringify(client.extract())};
</script>
${scriptTags}
</head>
`
)
.replace('<div id="app"></div>', `<div id="app">${body}</div>`);
res.send(html);
index.jsx
const SSRApp = (
<ApolloProvider client={ApolloClient}>
<ApplyTheme>
<BrowserRouter>
<App />
</BrowserRouter>
</ApplyTheme>
</ApolloProvider>
);
loadableReady(() => (
ReactDOM.hydrate(
SSRApp,
document.getElementById('app'),
)
));

It was my fault.
The hydration version of app contained BrowserRouter -> Switch -> Router -> HomePage
And the SSR version contained only StaticRouter -> HomePage
Because of this, after rendering SSR version, react removed all DOM and created new one with Router.

i changed in server.js. its worked for me
yours maybe (server/index.js or server.js or server/app.js..etc)
import Express from 'express';
import Loadable from 'react-loadable';
// from //
app.listen(3000, () => {
console.log('app now listening on port', port);
});
// to //
import Loadable from 'react-loadable';
Loadable.preloadAll().then(() => {
app.listen(port, () => {
console.log('app now listening on port', port);
});
});
for more config understanding you can see
The first step to rendering the correct content from the server is to make sure that all of your loadable components are already loaded when you go to render them.
To do this, you can use the Loadable.preloadAll method. It returns a promise that will resolve when all your loadable components are ready.

Related

Prevent falsy request from React Router loader function

I use React Router 6.4.2 and it's API - createBrowserRouter.
I have a default request that should be done only once when user reach main route ('/').
Request requires auth token (Amplify) so i use protected routes. No token - redirect to './auth/.
Router:
const router = createBrowserRouter([
{
element: (
<RequireAuth>
<AppLayout />
</RequireAuth>
),
children: [
{
path: '/',
loader: getDefaultData,
element: <MainPage />,
},
],
},
{ path: '/auth', element: <Authenticator /> },
{
path: '*',
element: <NotFoundPage />,
},
]);
export const AppRouter = () => {
return <RouterProvider router={router} fallbackElement={<AppLoader centered />} />;
};
RequireAuth:
export const RequireAuth = ({ children }: Props) => {
const location = useLocation();
const { route } = useAuthenticator((context) => [context.route]);
if (route !== 'authenticated') {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
return <>{children}</>;
};
GetDefaultData:
export const getDefaultData = async () => {
store.dispatch(
getData({
someVariables,
})
);
};
What i faced: when not authenticated user try to reach main route ('/'), he reach it for a moment before he will be redirected to './auth/ and React Router run getDefaultData from loader that fails on getting-auth-toker step.
What i expect: React Router will skip getDefaultData for that case. Looking for a way how to tell React Router about that in a beautiful way.
P.S. I know that i can add auth check with return inside getDefaultData function (generally it happens but not from scratch but on getting-auth-token).
I know about shouldRevalidate but not sure that it can help me in that case.
UPD. provided a codesandbox for that
https://codesandbox.io/s/amazing-matsumoto-cdbtow?file=/src/index.tsx
Simply try remove '/auth' from url manually and check console.
UPD. created an issue about that https://github.com/remix-run/react-router/issues/9529
Got an answer from Matt Brophy in github:
Fetching is decoupled from rendering in 6.4, so loaders run before any rendering logic. You should lift your "requires authorization" logic out of the RequireAuth component and into your loaders and redirect from there. Redirecting during render is too late 😄
Also note that all loaders for a matched route route run in parallel, so you should check in each loader for now (just like in Remix). We plan to make this easier to do in one place in the future.
Original answer:
https://github.com/remix-run/react-router/issues/9529

React/Node app not working on Chrome "Error running template: Invariant Violation: Invalid hook call"

I am getting an error in my react/node/meteor application. In particular, the app fails to load on Chrome, but works properly on all other browsers (edge, firefox). On Chrome I get a 500 error and the page does not load. On the terminal, where I am running the app, I get this:
(webapp_server.js:1010) Error running template: Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks...
I know that there are three common causes of this error as we see here:
https://reactjs.org/warnings/invalid-hook-call-warning.html
But I am using "react": "^16.8.0", "react-dom": "^16.8.0", and I don't use react native.
I don't believe I am improperly using hooks.
I ran this:
meteor npm ls react
and got back the following response:
`-- react#16.8.6
I have reset my machine and still the problem persists: Edge works fine, Chrome fails to load.
Here is the code that is having the error:
import React from 'react';
import MeteorLoadable from 'meteor/nemms:meteor-react-loadable';
import acceptLanguage from 'accept-language';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { Meteor } from 'meteor/meteor';
import { onPageLoad } from 'meteor/server-render';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { StaticRouter, Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import fetch from 'node-fetch';
import App from '/app/ui/components/smart/app';
import HeaderTitle from '/app/ui/components/smart/header/header-title';
import LanguagePicker from '/app/ui/components/dumb/language-picker';
import Routes from '/app/ui/routes';
import { primaryLocale, otherLocales } from '/app/intl';
const locales = primaryLocale ? [primaryLocale, ...otherLocales] : otherLocales;
acceptLanguage.languages(locales);
const render = async (sink) => {
const ssrClient = new ApolloClient({
link: new HttpLink({
uri: Meteor.absoluteUrl('/graphql'),
fetch,
}),
cache: new InMemoryCache(),
ssrMode: true,
});
const preferredLocale = acceptLanguage.get(sink.request.headers['accept-language']);
let locale = otherLocales.find(l => sink.request.url.path.startsWith(`/${l}`));
let prefix = locale;
// /app-shell is a special route that does no server-side rendering
// It's used by the service worker for all navigation routes, so after the first visit
// the initial server response is very quick to display the app shell, and the client
// adds in the data.
// In the case of a first visit or a robot, we render everything on the server.
if (sink.request.url.path === '/app-shell') {
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
return;
}
// We first check if we need to redirect to a locale
// We can only do this is there isn't a primary locale.
if (!primaryLocale) {
if (!locale) {
sink.setStatusCode(307);
sink.redirect(`/${preferredLocale || otherLocales[0]}${sink.request.url.path}`);
return;
}
} else if (!locale) {
// If there's no locale prefix, we use the primaryLocale instead
locale = primaryLocale;
prefix = '';
}
const ServerApp = ({ component, context }) => (
<MeteorLoadable.Capture>
<StaticRouter location={sink.request.url} context={context}>
<ApolloProvider client={ssrClient}>
<Route
path={`/${prefix}`}
render={props => <App component={component} {...props} locale={locale} section="app" />}
/>
</ApolloProvider>
</StaticRouter>
</MeteorLoadable.Capture>
);
// Load all data from local server
const context = {};
await getDataFromTree(<ServerApp component={Routes} context={context} />);
// Elements that we want rendered on the server
const sheet = new ServerStyleSheet();
sink.renderIntoElementById('header-title', renderToString(sheet.collectStyles(<ServerApp component={HeaderTitle} context={context} />)));
sink.renderIntoElementById('header-lang-picker', renderToString(sheet.collectStyles(<ServerApp component={LanguagePicker} context={context} />)));
sink.renderIntoElementById('main', renderToString(sheet.collectStyles(<ServerApp component={Routes} context={context} />)));
// Append helmet and styles
const helmetResult = Helmet.renderStatic();
['title', 'meta', 'link', 'script'].forEach(k => sink.appendToHead(helmetResult[k].toString()));
sink.appendToHead(sheet.getStyleTags());
// Append user's preferred locale
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
// Append Apollo data
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
// Append preloaded ReactLoadabe modules
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
};
Meteor.startup(async () => {
await MeteorLoadable.preloadComponents();
onPageLoad(render);
});
In particular it is this line that is returning the error:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
I commented that line out and the app seems to work fine now. I don't know if I need it and it should not be causing problems. This code is copied line for line from a starter kit and I don't know what this code is doing.
Note:
This code is taken from the following starter kit:
https://github.com/timothyarmes/ta-meteor-apollo-starter-kit
It looks like this did disobey hooks and so I changed the following code:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
To this:
const aServerApp = () => (
<ServerApp component={Routes} context={context} />
);
await getDataFromTree(aServerApp);
and it seems to work fine.

ElectronJS - sharing redux store between windows?

I have an electron app based on electron-react-boilerplate.
Now, that I have one window running as I wanted it to run, I started to create a new window.
I currently have 2 html files - one for each window - containing div roots:
<div data-root id="main_root"></div>
<div data-root id="second_root"></div>
My index.js file that is response for rendering the react app looks like this:
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import HomeRoot from './roots/HomeRoot';
import HoverRoot from './roots/HoverRoot';
import { configureStore, history } from './store/configureStore';
const store = configureStore();
const rootMapping = {
main_root: {
name: 'HomeRoot',
Component: HomeRoot,
getNextRoot: () => require('./roots/HomeRoot'),
},
second_root: {
name: 'SecondRoot',
Component: SecondRoot,
getNextRoot: () => require('./roots/SecondRoot'),
},
};
const renderDesiredRoot = () => {
const rootElementID = document.querySelector('[data-root]').id;
const root = rootMapping[rootElementID];
if (!root) throw Error('There is no such Root component!');
const { Component, getNextRoot, name } = root;
render(
<AppContainer>
<Component store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
if (module.hot) {
module.hot.accept(`./roots/${name}`, () => {
const NextRoot = getNextRoot();
render(
<AppContainer>
<NextRoot store={store} history={history} />
</AppContainer>,
document.getElementById(rootElementID),
);
});
}
};
renderDesiredRoot();
What it does, it checks which div root is available, and renders proper components.
My problem
How can I make a store that will be shared accross the BrowserWindow instances? I already looked into 2 npm packages (electron-redux and redux-electron-store) and they do not seem as a solution for me in this case.
I tried using this very simple approach, it works almost perfectly, but sometimes it's freezing (I'm not sure yet what exactly is making it to freeze). Maybe this could be useful to anyone, and if someone finds out what is causing the freezing issue, please let us know.
Redux store code (this same code is used by all windows):
export const store = window.opener?.store || createStore(...);
Object.assign(window, { store });
Then I need to open new electron window from a renderer process of the main window using:
const newWindow = window.open("/path", "someName");
And we also need this code on the main process:
win.webContents.on("new-window", function (e, url, frameName, _, options) {
e.preventDefault();
if (frameName === "someName")
e.newGuest = new BrowserWindow({ ...options, width: 300, height: 200, /* anything else you wanna add */ });
});
Nice solution:
You can use redux-state-sync which is a redux middleware that used to synchronize the redux store and actions across multiple react tabs, which works nicely with electron as the tabs here are the different renderer process.
The only hindrance is to initialize the store in the newly created window, that can be done by sending the store state with an ipc call from the main window that opens the new one, that dispatches an action to update the state.
This initialization approach works nicely in react#17.0.0 , but for some reason it doesn't in react react#18.0.0

ReferenceError on document event handling on ReactJS server side render

I'm working on a React.js app that must provide keyboard navigation on a horizontal scrolling carousel of items. On the current version, only left and right arrows are used to navigation and enter to make the selection. I'm mounting the listener on my container as:
const App = React.createClass({
componentWillMount() {
document.addEventListener("keydown", this.__onKeyDown);
},
__onKeyDown(event){
...
},
render: function() {
const items = []
Array(10).fill().map((_, i) => items.push(<MovieItem />
return (
<div className="scroller">
{items}
</div>
)
}
});
The above code works as expected until I tried to make it server side. I added the following route on my server config file and got: ReferenceError document is not defined
import { Server } from 'http';
import express from 'express';
import routes from '../routes';
var app = express();
app.get('*', (req, res) => {
match(
{ routes, location: req.url },
(err, redirectLocation, renderProps) => {
let markup;
if (renderProps) {
markup = renderToString(
<Provider store={store}>
<RouterContext {...renderProps}/>
</Provider>
);
}
return res.render('index', { markup });
}
);
});
It is clear to me why the error is happening, and that document is not available on the server. But what is the proper way to deal with it?
I have already tried to add a tabindex to the div that wraps the carousel and listen to div's onkeydown, but it just works when the div is focused.
Instead of adding event listener in componentWillMount, Use componentDidMount which executes only on client.
componentDidMount

ReactJS react-router RoutingContext

I'm building isomorphic application using ReactJS with react-router module for routing purposes on server side.
From its guide about using react-router on server:
(req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
//...
else if (renderProps) {
res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
}
//...
})
}
There is almost no information about this RoutingContext. So it's a bit unclear for me how it works. Is it some kind of replacement for Router component from react-router (used on top of other routes)?
Any help in understanding will be really appreciated!
React router v4
in the new version (v4) it has been updated to createServerRenderContext. This works in very different way than previously but is much more concise as it also get rid of the need for using 'match'.
this code example is to be applied as express middleware:
import React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerRouter/* , createServerRenderContext */ } from 'react-router';
// todo : remove line when this PR is live
// https://github.com/ReactTraining/react-router/pull/3820
import createServerRenderContext from 'react-router/createServerRenderContext';
import { makeRoutes } from '../../app/routes';
const createMarkup = (req, context) => renderToString(
<ServerRouter location={req.url} context={context} >
{makeRoutes()}
</ServerRouter>
);
const setRouterContext = (req, res, next) => {
const context = createServerRenderContext();
const markup = createMarkup(req, context);
const result = context.getResult();
if (result.redirect) {
res.redirect(301, result.redirect.pathname + result.redirect.search);
} else {
res.status(result.missed ? 404 : 200);
res.routerContext = (result.missed) ? createMarkup(req, context) : markup;
next();
}
};
export default setRouterContext;
react-lego is an example app that shows how to do universal rendering using createServerRenderContext
RoutingContext is an undocumented feature and will be replaced by RouterContext in v2.0.0. Its role is to synchronously render the route component.
It is simply a wrapper around your component which inject context properties such as history, location and params.
React router v4
in the new version (v4) it has been deleted to createServerRenderContext. This works in very different way than previously but is much more concise.
this little code example is to be applied.
import { StaticRouter } from'react-router-dom'
const context = {}
const mockup = renderToString(
<Provider store = {store}>
<IntlProvider locale = {locale} messages = {messages[locale]}>
<StaticRouter location={request.url} context={context}>
<ModuleReactWithPages />
</StaticRouter>
</IntlProvider>
</Provider>
)
Now it's a layer of itself when it's a 404

Categories

Resources