Recently, I intend to rewrite my personal homepage 叒 Yi, and this time I intend to try Gatsby. This is the background.

For those of you who don’t know what Gatsby is, it’s a static page-building tool based on React. By writing page templates (essentially React components) and configuration files, Gatsby can create pages for specific data files (Markdown, etc.).

I’ve been using serve mode throughout the development process, which is similar to WebPack Dev Server in that all routes will be rewrite to index.html and rendered entirely by the client. I added a lot of preferences to the app, like multilingual and night mode. Take multi-language as an example, the general idea is to write a Context as a scope, and then all the components under the scope can get the multi-language Context data through useContext.

Take a look at the code:

import React, { createContext, useState, useContext } from 'react';

import { setPref, getPref } from './globalPrefs';

const ctx = createContext({});

export function I18NScope(props) {
  const [currentLang, setCurrentLang] = useState(getPref('lang') | |'en');

  function _setCurrentLang(lang) {
    setPref('lang', lang);
    setCurrentLang(lang);
  }

  return (
    <ctx.Provider
      value={{
        currentLang.setCurrentLang: _setCurrentLang.stringMap: props.stringMap}} >
      {props.children}
    </ctx.Provider>
  );
}

export function useI18N(key) {
  const { currentLang, setCurrentLang, stringMap } = useContext(ctx);
  if (key) {
    return ((stringMap || {})[currentLang] || {})[key] || key;
  }
  return { currentLang, setCurrentLang };
}
Copy the code

It’s also easy to use:

function Post(props) {
  const { currentLang } = useI18N();
  const { currentStyle } = useTheme();

  const data = props.data;

  return (
    <>
      <div style={{ position: 'relative', paddingRight: '40px' }}>
        <Title text={data[currentLang].frontmatter.title} />
        <Paragraph>{data[currentLang].frontmatter.subtitle}</Paragraph>
        <Settings />
      </div>
      <div className={currentStyle.divider} />
      <div style={{ marginTop: '20px' }}>
        <article dangerouslySetInnerHTML={{ __html: data[currentLang].html }} />
      </div>
      <Links links={props.links} />
      <Footer />
    </>
  );
}
Copy the code

After the user sets the language, it will be synchronized to LocalStorage, and the default value of the context when the next application starts will be the value stored in LocalStorage, so it’s all very simple.

Everything is fine up here. When I finished writing a version to deploy and see the effect, I found that after setting the language and refreshing the page, the content is both Chinese and English, and English is the default language (that is, the HTML output language in SSR).

The English part is the article content under the Article tag and it looks like the dangerouslySetInnerHTML property was not handled during the Hydrate process. This is a React bug in my gut…

I did a quick search for Issues on GitHub and found none as dangerouslySetInnerHTML related as mine. Later I found that not only dangerouslySetInnerHTML is inconsistent, but also className is inconsistent. So I modified the keyword and continued searching, and finally found the issue #14281, which is exactly the phenomenon I described.

This isn’t a bug, it’s by Design. Simply put, React SSR used to re-render the entire page, so this problem doesn’t exist, but in the current version React assumes that THE SSR content is the same as the hydrate content. That is to say, I SSR out of HTML is what language, after running out should be what language. It is also easy to do this by adding routes for English and Chinese separately. The language is okay. What about the theme? If I add the size later, do I need to add routes for every combination? Obviously not.

Of course, there are ways to do this. As the React documentation says, a secondary render is fine. Because the SSR process does not trigger the effect for componentDidMount() and useEffect. So we can identify the current environment by a state. Once componentDidMount() or Effect is called, it means that the client is now rendering, and you can re-render using the Settings in LocalStorage.

Now that we have the method, all we need to do is simply modify our context component:

export function I18NScope(props) {
  const isClient = useClientEnv();  // Add this state
  const [currentLang, setCurrentLang] = useState(getPref('lang') | |'en');

  function _setCurrentLang(lang) {
    setPref('lang', lang);
    setCurrentLang(lang);
  }

  return (
    <ctx.Provider
      value={{
        currentLang: isClient ? currentLang : 'en',
        setCurrentLang: _setCurrentLang.stringMap: props.stringMap}} >
      {props.children}
    </ctx.Provider>
  );
}
Copy the code

UseClientEnv is a custom hook:

import { useState, useEffect } from 'react';

export function useClientEnv() {
  const [isClient, setIsClient] = useState(false);
  useEffect((a)= > {
    setIsClient(true); } []);return isClient;
}
Copy the code

Redeploy, problem solved.

TL; DR

SSR should be the same as the first client render, if there must be some inconsistency, then render the latest content on the second render.

SSR currently has two main purposes, one is to reduce the first screen wait time, so for this purpose, we can render the minimum amount of content on the server side, such as only skeleton.

The other is for SEO, so the server needs to render the actual content of the page, for the above multi-language case, in fact, the best practice is to use routing control display language version, this also helps the search engine to crawl the content, you do not want users to search out Chinese, click in English it. Preferences such as theme and font size can be synchronized with a second rendering, but this introduces another problem: page flickering. The page is re-rendered as soon as JS is finished loading. Even if JS is cached, there will be some time between the HTML loading and JS loading and execution. A simple optimization can be made here: hide the content with CSS and start a timer in an inline script tag. When the timer expires, the content will be displayed in case the first JS bundle takes too long to load. In the later stage, JS bundles and related resources can be cached through Service workers, etc. Then when entering the page, JS resources can be loaded and executed in a short time because they are cached.

Cyandevio. Unixzii.now.sh