preface

The stack of technologies I work on is React + TypeScript, so in this article I’d like to summarize some of the techniques used in React projects for performance optimization or better code organization.

The importance of performance optimization goes without saying. Google has published a number of studies that accurately show the impact of performance on site retention, and code organization optimization is related to subsequent maintenance costs and how often your colleagues “spit” when maintaining your code 😁, you’ll learn something by the end of this article.

This article was first published on The front end from Progression to admission. Focus on me and take you to the next level

The magic of the children

We have a requirement to pass some topic information to child components via the Provider:

Look at this code:

import React, { useContext, useState } from "react";

const ThemeContext = React.createContext();

export function ChildNonTheme() {
  console.log("Child component rendered without care of skin");
  return <div>I don't care about the skin, don't make me re-render when the skin changes!</div>;
}

export function ChildWithTheme() {
  const theme = useContext(ThemeContext);
  return <div>I have skin ~ {theme}</div>;
}

export default function App() {
  const [theme, setTheme] = useState("light");
  const onChangeTheme = () = > setTheme(theme === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={onChangeTheme}>Change the skin</button>
      <ChildWithTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
    </ThemeContext.Provider>
  );
}
Copy the code

This code looks fine, and is intuitive enough to roll up your sleeves, but it causes ChildNonTheme, a child component that doesn’t care about skin, to ineffectually rerender when the skin state changes.

This is essentially because React is a top-down recursive update. Code like
will be translated by Babel into function calls like React. CreateElement (ChildNonTheme), React often insists that props are immutable, so a new props reference is generated every time a functional component is called.

Look at the return structure of createElement:

const childNonThemeElement = {
  type: 'ChildNonTheme'.props: {} // <- This reference is updated
}
Copy the code

It is because of this new props reference that the ChildNonTheme component is also re-rendered.

So how do you avoid this invalid re-rendering? The key word is “clever use of children”.

import React, { useContext, useState } from "react";

const ThemeContext = React.createContext();

function ChildNonTheme() {
  console.log("Child component rendered without care of skin");
  return <div>I don't care about the skin, don't make me re-render when the skin changes!</div>;
}

function ChildWithTheme() {
  const theme = useContext(ThemeContext);
  return <div>I have skin ~ {theme}</div>;
}

function ThemeApp({ children }) {
  const [theme, setTheme] = useState("light");
  const onChangeTheme = () = > setTheme(theme === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={onChangeTheme}>Change the skin</button>
      {children}
    </ThemeContext.Provider>
  );
}

export default function App() {
  return (
    <ThemeApp>
      <ChildWithTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
    </ThemeApp>
  );
}
Copy the code

Yeah, the only difference is that I took the state control component and the presentation subcomponent out of it and rendered it directly through children, because children came in from outside, There will be no react. createElement code inside ThemeApp. Children will not change after the setTheme is rerendered, so they can be reused.

Look again at the
wrapped in ThemeApp. This is passed to the ThemeApp as children. Updates inside the ThemeApp do not trigger the external react.createElement at all. So the result is to reuse the previous element directly:

// Fully multiplexed, props will not change.
const childNonThemeElement = {
  type: ChildNonTheme,
  props: {}}Copy the code

After the skin change, the console is empty! Optimization is achieved.

To sum up, it is necessary to promote the child components that take time to render but don’t need to care about state to the outside of the stateful component and pass them in as children or props to be used directly to prevent them from being rendered with them.

Magical Children – Online debugging address

Of course, this optimization can also be done with the React. Memo wrapped sub-component, but the maintenance cost is relatively increased.

Context read-write separation

Imagine that now we have a global logging requirement, we want to do it with a Provider, and soon the code is ready:

import React, { useContext, useState } from "react";
import "./styles.css";

const LogContext = React.createContext();

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = (log) = > setLogs((prevLogs) = > [...prevLogs, log]);
  return (
    <LogContext.Provider value={{ logs.addLog}} >
      {children}
    </LogContext.Provider>
  );
}

function Logger1() {
  const { addLog } = useContext(LogContext);
  console.log('Logger1 render')
  return (
    <>
      <p>A logging component 1</p>
      <button onClick={()= >AddLog (" logger1 ")} > log</button>
    </>
  );
}

function Logger2() {
  const { addLog } = useContext(LogContext);
  console.log('Logger2 render')
  return (
    <>
      <p>A logging component 2</p>
      <button onClick={()= >AddLog (" logger2 ")} > log</button>
    </>
  );
}

function LogsPanel() {
  const { logs } = useContext(LogContext);
  return logs.map((log, index) = > <p key={index}>{log}</p>);
}

export default function App() {
  return (
    <LogProvider>{/* Write a log */}<Logger1 />
      <Logger2 />{/* Read log */}<LogsPanel />
      </div>
    </LogProvider>
  );
}
Copy the code

We’ve used the optimization tips of the previous chapter to wrap the LogProvider separately and push the child components to the outer layer.

For the best case scenario, the Logger component only issues logs, it doesn’t care about changes in logs, and ideally only the LogsPanel component should be re-rendered when any component calls addLog to write to the log.

However, such code writing causes all loggers and LogsPanel to be re-rendered every time any component writes to the log.

This is certainly not what we would expect, given that in real world code, there are so many components that can log that every write causes global components to be rerendered, right? This is of course unacceptable, and the nature of the problem is clear from the Context section on the website:

When the LogProvider addLog quilt component is called and the LogProvider is rerendered, the value passed to the Provider must change. Since the value contains the logs and setLogs properties, So a change in either of these will cause all of the logProvider-subscribed child components to be re-rendered.

So what’s the solution? We pass logs (read) and setLogs (write) through different providers, so that the component responsible for writing changes logs, and the other “write components” are not re-rendered, only the “read components” that really care about logs are re-rendered.

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = useCallback((log) = > {
    setLogs((prevLogs) = >[...prevLogs, log]); } []);return (
    <LogDispatcherContext.Provider value={addLog}>
      <LogStateContext.Provider value={logs}>
        {children}
      </LogStateContext.Provider>
    </LogDispatcherContext.Provider>
  );
}
Copy the code

As we mentioned earlier, we need to make sure that the value reference doesn’t change, so we naturally wrap the addLog method in useCallback so that when the LogProvider rerenders, The value passed to the LogDispatcherContext does not change.

Now WHEN I send a log from any “write component”, it only renders the “read component” LogsPanel.

Context Read/write separation – Online debugging

Context code organization

In the above case, we get the global state in the child component, using useContext directly:

import React from 'react'
import { LogStateContext } from './context'

function App() {
  const logs = React.useContext(LogStateContext)
}
Copy the code

But is there a better way to organize your code? Like this:

import React from 'react'
import { useLogState } from './context'

function App() {
  const logs = useLogState()
}
Copy the code
// context
import React from 'react'

const LogStateContext = React.createContext();

export function useLogState() {
  return React.useContext(LogStateContext)
}
Copy the code

Plus a little bit of robustness, right?

import React from 'react'

const LogStateContext = React.createContext();
const LogDispatcherContext = React.createContext();

export function useLogState() {
  const context = React.useContext(LogStateContext)
  if (context === undefined) {
    throw new Error('useLogState must be used within a LogStateProvider')}return context
}

export function useLogDispatcher() {
  const context = React.useContext(LogDispatcherContext)
  if (context === undefined) {
    throw new Error('useLogDispatcher must be used within a LogDispatcherContext')}return context
}
Copy the code

If some component needs to read and write the log at the same time, calling it twice is cumbersome?

export function useLogs() {
  return [useLogState(), useLogDispatcher()]
}
Copy the code
export function App() {
  const [logs, addLogs] = useLogs()
  // ...
}
Copy the code

Depending on the scenario, use these techniques flexibly to make your code more robust and elegant

Combination will

Suppose we use the above method to manage some global small states, and the Provider becomes more and more numerous, and sometimes we encounter a nested hell:

const StateProviders = ({ children }) = > (
  <LogProvider>
    <UserProvider>
      <MenuProvider>
        <AppProvider>
          {children}
        </AppProvider>
      </MenuProvider>
    </UserProvider>
  </LogProvider>
)

function App() {
  return (
    <StateProviders>
      <Main />
    </StateProviders>)}Copy the code

Is there a way around it? Of course there is, let’s reference the compose method in Redux and write a composeProvider method of our own:

function composeProviders(. providers) {
  return ({ children }) = >
    providers.reduce(
      (prev, Provider) = > <Provider>{prev}</Provider>,
      children,
    )
}
Copy the code

The code can be simplified like this:

const StateProviders = composeProviders(
  LogProvider,
  UserProvider,
  MenuProvider,
  AppProvider,
)

function App() {
  return (
    <StateProvider>
      <Main />
    </StateProvider>)}Copy the code

conclusion

This article focuses on the Context API, and introduces several optimization points for performance and code organization, which can be summarized as follows:

  1. Try to elevate render irrelevant child component elements outside of the stateful component.
  2. Separate the Context from the read and write if necessary.
  3. Wrap the use of the Context and pay attention to error handling.
  4. Combine multiple contexts, optimize your code.

Welcome to “front end from advanced to hospital”, there are a lot of front end original articles oh ~