How does React.suspense "handle" i18next backend loading? - javascript

I'm working with some pretty standard create-react-app boilerplate, which uses lazy loading and react-i18next as a translation library. This library uses i18next-http-backend to fetch the translation files from a remote API.
What i'm trying to understand, is how exactly React.suspense is able to "recognize" this asynchronous call, and show the fallback UI until it's done.
Index.ts file:
import "./i18n";//Notice this
const container = document.getElementById("root");
ReactDOM.render(
<StylesProvider jss={jss}>
<ThemeProvider theme={theme}>
<React.StrictMode>
<BrowserRouter>
<Router />
</BrowserRouter>
</React.StrictMode>
</ThemeProvider>
</StylesProvider>,
container
);
i18n file:
i18n
.use(Backend)
.init({
backend:{
loadPath: 'https://someRemoteApi/dictionary',
})
Router:
const Home = lazy(() => import("../../modules/home/Home"));
const Router: React.FC = (props) => {
return (
<>
<ErrorBoundary>
<Suspense fallback={<div className={styles.loader}><Loader /></div>}>
<Switch>
<ProtectedRoute exact component={Home} path="/"/>
...more routes
</Suspense>
</ErrorBoundary>
</>
);
};
With this setup, to my amazement, the fallback is rendered on the screen, until this backend plugin finishes its job. I'm trying to understand the mechanics of it, and whether this can be leveraged for other async operations.
the React docs clearly state:
React.Suspense lets you specify the loading indicator in case some
components in the tree below it are not yet ready to render. Today,
lazy loading components is the only use case supported by
<React.Suspense>:
Any clarification will be greatly appreciated.

The idea of Suspense is when a component throws a Promise (or anything that is called during the component’s render), React looks for the closest Suspense in order to display the fallback UI.
In your case, your components are using the useTranslate hook. When namespaces are not yet loaded, it throws a Promise and loads the namespaces. During the render phase, React catches the thrown Promise and looks for the closest Suspense component up the tree in order to display fallback UI.
This is a snippet from the hook useTranslation:
// not yet loaded namespaces -> load them -> and trigger suspense
throw new Promise((resolve) => {
loadNamespaces(i18n, namespaces, () => {
resolve();
});
});
You can check how the hook useTranslation works from here

Related

Is this the correct way of using React.Suspense?

I have several questions about React.Suspense.
Here is some example code for reference:
// components/ComponentA/index.js
import React from "react";
const style = {
display: "grid",
placeItems: "center"
};
export default function ComponentA() {
return <div style={style}>I am componentA</div>;
}
// components/ComponentB/index.js
import React from "react";
const style = {
display: "grid",
placeItems: "center"
};
export default function ComponentB() {
return <div style={style}>I am componentB</div>;
}
// App.js
import React from "react";
import "./styles.css";
const ComponentA = React.lazy(() => import("./components/ComponentA"));
const ComponentB = React.lazy(() => import("./components/ComponentB"));
export default function App() {
const [state, toggleState] = React.useReducer((state) => !state, false);
return (
<div className="App">
<button onClick={toggleState}>click to show</button>
<React.Suspense fallback={<div>Loading...</div>}>
{state ? <ComponentA /> : <ComponentB />}
</React.Suspense>
</div>
);
}
When running this code, I've noticed several things right away.
The first thing I noticed is that when the app loads for the first time, there is a split moment where you can see loading... before I am componentB is rendered.
The second thing I noticed is that if you click the button, you can, again, for a split moment see loading... before I am componentA is rendered on the screen.
I assume this is the expected result with dynamic imports.
The final thing that I noticed is that continuing to toggle the button, I never see loading... again. I assume that this is because the components have already been imported and used by the client. Which is, I also assume, expected behavior.
My three questions here are,
am I using React's dynamic imports correctly (or should I use import elsewhere) and,
when should React.Suspense and dynamic imports be used
If the components are relatively simple, do I need to even consider lazy loading components?
Sandbox for reference
For the First question -
Yes, you are using the lazy() function to import components Correctly.
For the second question -
From the React docs,
Suspense lets components “wait” for something before rendering.
It is used to improve your site's performance with regards to its First Contentful Paint and Loading times on slow or bad networks, as well as for asynchronous tasks like data fetching where your component depends on the data returned so you show a Loading.. message to the user as a Fallback.
For example, a component which I have written and used-
import React, { Component, lazy, Suspense } from "react";
import { Route, Switch } from "react-router-dom";
import Error404 from "./Error404";
import LoadingSpinner from "./LoadingSpinner";
const Dashboard = lazy(() => import("./Dashboard"));
const Employees = lazy(() => import("./EmployeesPage"));
class Container extends Component {
//handles paths '/' and '/employees' along with any unknown paths
render() {
return (
<div id="container">
<div id="background">
</div>
<div id="content">
<Switch>
<Route exact path="/">
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
</Route>
<Route path="/employees">
<Suspense fallback={<LoadingSpinner />}>
<Employees />
</Suspense>
</Route>
<Route component={Error404} />
</Switch>
</div>
</div>
);
}
}
export default Container;
Here, I am lazy loading my two components Dashboard and Employees which in themselves contain many components and are relatively complex.
Lazy loading them prevents the connection to be clogged while loading the site for the first time.
These components are now loaded only when the user navigates to the specified URL by some NavLinks.
Since it's safe to assume that the user will spend some time in the Dashboard before going to the Employees page(at least in my app), this approach works fine.
For the third question -
If you have components that you are sure will be needed when the site loads, it's better to not lazy load them to keep the User Experience good.
Small components (by their bundle size) don't need lazy loading, only when you think that many components are gonna be used only rarely, then you can lazy load them in one bundle.
Lastly -
You can open up the Network tab on the developer tools of a browser and see the bundles loading in as the user requests for them (React handles that in lazy()). The Bundles are labelled like this- Network Tab screenshot
Hope you find the answer useful and any recommendations for the post are appreciated✌

React Router V4 protected private route with Redux-persist and React-snapshot

I'm implementing private route like so using React Router Route Component:
function PrivateRoute({component: Component, authed, emailVerified, ...rest}) {
return (
<Route
{...rest}
render={props =>
authed === true
? <Component {...props} />
: <Redirect to={{pathname: '/', state: {from: props.location}}} />}/>
)
}
Expected Behavior:
authed is persisted through page refresh through using redux-persist
So on page refresh or reload, if authed prop is true then router should render <Component /> without ever going to path "/"
Actual Behavior which is the Problem:
With authed === true (persisted)
reloading the page or refreshing it leads to the following actions taking place(checked redux devtools)
the action:
"##router/LOCATION_CHANGE" runs and takes it to the correct secure route but then "##router/LOCATION_CHANGE" runs again and it redirects to "/" for a moment and finally
"##router/LOCATION_CHANGE" runs again and directs route back to the secure path, even though authed === true through all this in the redux devtools
Then: My guess was that this error has something to with my main App Component rendering before redux-persist has time to re-hydrate the Redux store.
So I tried doing the following:
I tried delaying my main App component render until my store is re-hydrated using redux-persist like so:
render() {
const {authed, authedId, location, rehydrationComplete} = this.props
return (
<div>
{ rehydrationComplete
? <MainContainer>
<Switch key={location.key} location={location}>
<Route exact={true} path='/' component={HomeContainer} />
<Route render={() => <h2> Oops. Page not found. </h2>} />
</Switch>
</MainContainer>
: <div>...Loading </div> }
</div>
)
}
This effectively fixes the issue above of the path changing when "##router/LOCATION_CHANGE" action runs(only Changes the path keys), However this leads to another Issue with React-snapshot Now: all the static generated html files from react-snapshot Now contain only ...Loading. I tried to set snapshotDelay of 8200 in the react-snapshot options but that didnt solve the issue.
Then:
I tried the following to delay React-snapshot call so that it renders html after the store has been rehydrated:
import {render as snapshotRender} from 'react-snapshot'
import {ConnectedRouter} from 'react-router-redux'
async function init() {
const store = await configureStore()
snapshotRender(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)
registerServiceWorker()
}
init()
But now i get the error: that 'render' from react-snapshot was never called. Did you replace the call to ReactDOM.render()?
I know this is a loaded question, but I want to effectively use these 3 libs(React-Router V4, Redux-persist, React-snapshot) together to serve protected routes without the mentioned errors.
I have something similar to you. Here I use React-Router V4 and a persist-like library.
Your router/routes doesn't need to be aware of the persist. They should rely on your redux's store. The persist should rehydrate your store with all the data.
I didn't see where you are using the PrivateRoute component in your example. Where is it?

React routes not automatically updating when nested under 'smart' Redux container components

I'm trying to create an Electron app using React, React-router and Redux. What I'm finding is that my routing logic works absolutely fine when I'm nesting the switch/route logic under a purely presentational component (Page), but that I'm forced to refresh the page to see navigational changes if nested under a 'smart' container component.
Near the top of my React component hierarchy (right beneath HashRouter) I have a Page:
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
<DetailPane>{props.children}</DetailPane>
</div>
);
}
Here, DetailPane and SideBar are both container components wrapped around presentational components of the same name.
At startup (and during hot reloads), I create my React hierarchy using this function:
export default () => (
<Router>
<Page>
<Switch>
<Route exact path='/txDefinitions/:definitionName/:fieldName' component={FieldPage}/>
<Route exact path='/txDefinitions/:definitionName?' component={DefinitionPage}/>
<Route exact path='/rxDefinitions/:definitionName?' component={DefinitionPage}/>
<Route exact path='/'/>
<Route component={Route404}/>
</Switch>
</Page>
</Router>
This means that <Switch>...</Switch> gets nested underneath <DetailPane>.
If I try to navigate around my app (clicking links in the side bar), I won't actually see the detail pane render the new component until I force-reload the Electron app.
However, I find that routing works as expected if I omit DetailPane from Page:
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
{props.children}
</div>
);
}
Here is my React hierarchy without DetailPane (works fine):
Here is my React hierarchy with DetailPane (does not work right):
(Apologies for using images but I'm not sure if there's a way to copy from React devtools into clipboard - appears larger if opened in a new tab).
As I was writing this question, I realised this wouldn't be a huge issue for me because earlier refactoring had made the 'smart' version of DetailPane apparently obsolete. Using the purely presentational version of DetailPane
instead resolves this issue:
import * as React from 'react';
//import {DetailPane} from '../../containers'; // Smart/Redux
import {DetailPane} from '../../components'; // Dumb/presentational
import {SideBar} from '../../containers/';
const styles = require('./Page.scss');
export default function Page (props) {
return (
<div className={`${styles.page}`}>
<SideBar/>
<DetailPane>{props.children}</DetailPane>
</div>
);
}
However, I'm still curious why this doesn't work for the container component version. For reference, this is the container component version of DetailPane:
import {connect} from 'react-redux';
import {DetailPane} from '../../components';
// TODO: delete this container?
function mapStateToProps (state): {} {
return {};
}
function mapDispatchToProps (dispatch) {
// TODO.
return {};
}
export default connect(mapStateToProps, mapDispatchToProps)(DetailPane);
The connect HOC implements shouldComponentUpdate logic so if the props don't change, the component doesn't update.
To prevent this from occurring, and have the component always render, you can override the pure option in the connect call.
export default connect(mapStateToProps, mapDispatchToProps, undefined, { pure: false })(DetailPane);
See the react-redux API docs for more details.

Apollo + React Router 4 SSR issue

Not sure if this is an issue with React Router v4, the React Apollo client or my implementation.
But with <ApolloProvider> as the top-level HOC, i.e:
const ComponentsWithData = await getDataFromTree(
<ApolloProvider client={apolloClient}>
<StaticRouter location={ctx.request.url} context={route}>
<App />
</StaticRouter>,
</ApolloProvider>,
);
const html = ReactDOMServer.renderToString(ComponentsWithData);
... I get:
Warning: Failed prop type: Invalid prop children of type array supplied to ApolloProvider, expected a single ReactElement.
in ApolloProvider
Error React.Children.only expected to receive a single React element child.
And flipped around, with React Router's <StaticRouter> as the top, i.e.:
const ComponentsWithData = await getDataFromTree(
<ApolloProvider client={apolloClient}>
<StaticRouter location={ctx.request.url} context={route}>
<App />
</StaticRouter>,
</ApolloProvider>,
);
... I then get:
A <Router> may have only one child element
Rendering works fine in the browser (with React Router's <BrowserRouter>), but fails on the server.
It also works well in React Router v3 due to doing all of the route matching outside of the React hierarchy, and not declaratively inside of it.
This was actually a user error. I expected getDataFromTree() to return a Promise that resolved to the original component chain (with the correctly injected GraphQL data props), but it actually just waits for the data to be ready.
The correct format is:
const Components = (
<StaticRouter location={ctx.request.url} context={route}>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</StaticRouter>
);
await getDataFromTree(Components);
const html = ReactDOMServer.renderToString(Components);

Passing props from react router to children on the server

I'm building an Isomorphic app using React, react-router v3 and material-ui. One of the requirements of material-ui in server-side rendering is to pass to the theme the client's user agent, so MUI will be able to prefix their inline styles accordingly.
Originally the root component of the app was the router, i.e on the server side:
<RouterContext {...renderProps}/>
And on the client:
<Router children={routes} history={browserHistory} />
Now, since I didn't know how to pass the user agent to the RouterContext on the server, I came up with an (ugly?) solution: I've created a useless component named Root, to whom I passed the user agent, and Root had the router as his children, i.e. on the server side:
<Root userAgent={userAgent}>
<RouterContext {...renderProps}/>
</Root>
and on the client:
<Root>
<Router children={routes} history={browserHistory} />
</Root>
Now, everything works well but I don't really like creating that useless element if I don't have to, so my question is - is there a better way?
Can I somehow pass the user agent to RouterContext on the server, which will in turn pass it to the Index Route which will create the theme?
Here's the code of Root if someone's interested:
class Root extends React.Component {
constructor(props){
super();
this.muiTheme = getMuiTheme(customTheme, { userAgent: props.userAgent });
}
render () {
return (
<MuiThemeProvider muiTheme={this.muiTheme}>
{this.props.children}
</MuiThemeProvider>
);
}
}
You can use createElement on RouterContext to achieve this.
<RouterContext
{...renderProps}
createElement={(Component, props) => <Component {...props} userAgent={data.userAgent} />}
/>
This will make userAgent available in the props of all route components.

Categories

Resources