Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suspense #3

Merged
merged 3 commits into from
Dec 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Based on original [Lingui tutorial](https://lingui.js.org/tutorials/setup-cra.ht
* [Reimplement `LanguageSwitcher` for React Router](#reimplement-languageswitcher-for-react-router)
- [Add meta tags with the help of React Helmet](#add-meta-tags-with-the-help-of-react-helmet)
- [Add prerendering with the help of react-snap](#add-prerendering-with-the-help-of-react-snap)
* [Flash of the white screen](#flash-of-the-white-screen)
* [ConcurentMode](#concurentmode)

<!-- tocstop -->

Expand Down Expand Up @@ -484,3 +486,70 @@ Add `postbuild` hook to the `package.json`:
```

And you done!

### Flash of the white screen

On the one side, we have prerendered HTML which will start to render as soon as the browser will get it (around 2s in the US on average 3G). On the other side, we have React which will start to render as soon as all scripts will be downloaded (around 3s in the US on average 3G, for the given example).

When React will start to render and if not all dynamic resources will be loaded it will flush all the content it has and typically this is the almost white (empty) screen. This where we get "Flash of the white screen". Dynamic resources can be: async components (`React.lazy(() => import())`), locale catalog (`import("./locales/" + locale + "/messages.js");`).

It looks like this:

![](public/filmstrip-flash.png)

To solve the problem we need to wait for all resources to load before React will flush the changes to the DOM.

We can do this with loader library like, `react-loadable` or `loadable-components`. See more details [here](https://github.com/stereobooster/react-snap#async-components).

Or we can do this with new `React.lazy`, `<Suspense />` and `<ConcurentMode />`.

### ConcurentMode

`<ConcurentMode />` marked as unstable (use at your own risk), so it can change in the future. Read more on how to use it and about caveats [here](https://github.com/stereobooster/react-async-issue).

```js
const ConcurrentMode = React.unstable_ConcurrentMode;
const RootApp = (
<ConcurrentMode>
<Suspense fallback={<div>Loading...</div>} maxDuration={5000}>
<App />
</Suspense>
</ConcurrentMode>
);
const rootElement = document.getElementById("root");
const root = ReactDom.unstable_createRoot(rootElement, { hydrate: true });
root.render(RootApp);
```

This is the first hack we need.

The second one is that we need to repurpose `React.lazy` to wait for subresource. React team will eventually add `Cache` for this, but for now, let's keep hacking.

```js
const cache = {};
export default ({ locale, children }) => {
const SuspendChildren =
cache[locale] ||
React.lazy(() =>
i18n.activate(locale).then(() => ({
__esModule: true,
default: ({ children }) => (
<I18nProvider i18n={i18n}>{children}</I18nProvider>
)
}))
);
cache[locale] = SuspendChildren;
return <SuspendChildren>{children}</SuspendChildren>;
};
```

- `i18n.activate(locale)` returns promise, which we "convert to ES6" module e.g. `i18n.activate(locale).then(() => ({ __esModule: true, ...}))` is equivalent to `import()`.
- `default: ...` - default export of pseudo ES6 module
- `({children}) => <I18nProvider i18n={i18n}>{children}</I18nProvider>` react functional component
- `<SuspendChildren />` will tell `<Suspense />` at the top level to pause rendering until language catalog is loaded

`<ConcurentMode />` will enable `<StrictMode />` and it will complain about unsafe methods in `react-router`, `react-router-dom`. So we will need to update to beta version in which issue is fixed. [`react-helmet` also incompatible with `<StrictMode />`](https://github.com/nfl/react-helmet/issues/426), so we need to replace it with `react-helmet-async`.

One way or another but we "fixed" it.

![](public/filmstrip-no-flash.png)
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"@lingui/react": "^3.0.0-3",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-helmet": "^5.2.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-helmet-async": "^0.2.0",
"react-router": "next",
"react-router-dom": "next",
"react-scripts": "2.1.1"
},
"scripts": {
Expand All @@ -21,7 +21,7 @@
"extract": "lingui extract",
"compile": "lingui compile",
"deploy": "gh-pages -d ./build",
"markdown-toc": "markdown-toc -i ./README.md",
"toc": "markdown-toc -i ./README.md",
"postbuild": "react-snap"
},
"eslintConfig": {
Expand Down
Binary file added public/filmstrip-flash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/filmstrip-no-flash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import React, { Component } from "react";
import { Route, Switch, BrowserRouter, Redirect } from "react-router-dom";
import { I18nProvider } from "@lingui/react";
import { i18n, defaultLocale, supportedLocale } from "./i18n";
import Home from "./Home";
import NotFound from "./NotFound";
import { basePath } from "./config";
import I18nLoader from "./I18nLoader";
import { HelmetProvider } from 'react-helmet-async';

const I18nRoutes = ({ match }) => {
let { locale } = match.params;

if (!supportedLocale(locale)) {
i18n.activate(i18n.locale || defaultLocale);
return (
<I18nProvider i18n={i18n}>
<I18nLoader locale={i18n.locale || defaultLocale}>
<Route component={NotFound} />
</I18nProvider>
</I18nLoader>
);
}

i18n.activate(locale);
return (
<I18nProvider i18n={i18n}>
<I18nLoader locale={locale}>
<Switch>
<Route path={`${match.path}/`} component={Home} exact />
<Route component={NotFound} />
</Switch>
</I18nProvider>
</I18nLoader>
);
};

Expand All @@ -42,12 +41,14 @@ const RootRedirect = () => {
class App extends Component {
render() {
return (
<HelmetProvider>
<BrowserRouter>
<Switch>
<Route path={`${basePath}/`} component={RootRedirect} exact />
<Route path={`${basePath}/:locale`} component={I18nRoutes} />
</Switch>
</BrowserRouter>
</HelmetProvider>
);
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/I18nLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { I18nProvider } from "@lingui/react";
import { i18n } from "./i18n";

const cache = {};
export default ({ locale, children }) => {
const SuspendChildren =
cache[locale] ||
React.lazy(() =>
i18n.activate(locale).then(() => ({
__esModule: true,
default: ({ children }) => (
<I18nProvider i18n={i18n}>{children}</I18nProvider>
)
}))
);
cache[locale] = SuspendChildren;
return <SuspendChildren>{children}</SuspendChildren>;
};
2 changes: 1 addition & 1 deletion src/Meta.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { Route } from "react-router-dom";
import Helmet from "react-helmet";
import Helmet from 'react-helmet-async';

export default ({ locales }) => (
<Route
Expand Down
13 changes: 11 additions & 2 deletions src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ export const locales = {
};
export const defaultLocale = "en";

function loadCatalog(locale) {
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));

const loadCatalog = async locale => {
if (
navigator.userAgent !== "ReactSnap" &&
process.env.NODE_ENV !== "development"
) {
// intentionally slow translations to simmulate bigger JS bundle
await pause(500);
}
return import(/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`./locales/${locale}/messages.js`);
}
};

export const i18n = setupI18n();
i18n.willActivate(loadCatalog);
Expand Down
23 changes: 17 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import React, { Suspense } from "react";
import ReactDom from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(<App />, document.getElementById('root'));
const ConcurrentMode = React.unstable_ConcurrentMode;
const RootApp = (
<ConcurrentMode>
<Suspense fallback={<div>Loading...</div>} maxDuration={5000}>
<App />
</Suspense>
</ConcurrentMode>
);

const rootElement = document.getElementById("root");
const root = ReactDom.unstable_createRoot(rootElement, { hydrate: true });
root.render(RootApp);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
Expand Down
Loading