Helux 2 released to solve the side effect double call mechanism of react18

fantasticsoul

幻魂

Posted on May 30, 2023

Helux 2 released to solve the side effect double call mechanism of react18

react 18 has added a heuristic concurrent rendering mechanism. The side effect function may be called multiple times due to component re-rendering. In order to help users clarify the correct way to use side effects, when StrictMode is enabled in the development mode, it will be deliberately called twice The side effect function is used to achieve the effect of checking user logic, but this also brings troubles to some users who upgrade. This article will discuss how helux can avoid this problem.

Introduction to helux

helux is a react state library that focuses on lightweight, high-performance, and zero-cost access. Your application only needs to replace useState is useShared, and then you can achieve the effect of upgrading the local state of react to a global shared state without modifying a single line of other codes. You can visit this online example to learn more.

import React from 'react';
+ import { createShared, useShared } from 'helux';
+ const { state: sharedObj } = createShared({a:100, b:2});

function HelloHelux(props: any) {
- const [state, setState] = React. useState({ a: 100, b: 2 });
+ const [state, setState] = useShared(sharedObj);

    // The current component only depends on a change to trigger re-rendering
    // helux will dynamically collect the latest dependencies of each round of rendering of the current component to ensure accurate updates
    return <div>{state.a}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The default shared object is non-responsive. It is expected that the user will change the state in a react way. For example, after the user sets enableReactive to true, a responsive object can be created

const { state, setState } = createShared({ a: 100, b: 2 }, true);
// or
const { state, setState } = createShared({ a: 100, b: 2 }, { enableReactive: true });

// will update all component instances using `sharedObj.a` values
sharedObj.a++;
setState({a: 1000});
Enter fullscreen mode Exit fullscreen mode

What 2.0 brings

The 2.0 version has made the following three adjustments

Simplified api naming

The original useSharedObject api is re-exported as a more streamlined useShared, which cooperates with createShared to improve the user's writing efficiency and reading experience.

Add signal record (experimental)

Added signal-related record data internally to prepare the relevant infrastructure for helux-signal (a react signal mode implementation library based on helux packaging) to be released in the future. helux-signal is still in the prototype stage. The timing of the beta version experience will be released.

When not using signals, you need createShared and useShared to match the two together. createShared creates the shared state, and useShared is responsible for consuming the shared state. It returns specific readable state values and update functions.

 const { state: sharedObj } = createShared({a:100, b:2}); // create
 function HelloHelux(props: any) {
   const [state, setState] = useShared(sharedObj); // use
   // render logic...
 }
Enter fullscreen mode Exit fullscreen mode

When using a signal, you only need to call helux-signal an interface createSignal to complete the creation of the state, and then the component can skip the useShared hook function and directly read the shared state.

It should be noted that at present, helux 2 has only completed the relevant infrastructure internally, and the helux-signal, which is responsible for the specific implementation of the upper layer, is still in the experimental stage. At this stage, the community has already provided signals-react library to support the development of react components in signal mode.

An envisioned and complete example of developing react components based on helux-signal is as follows:

 import { createSignal, comp } from 'helux-signal';

 const { state, setState } = createSignal({a:100, b:2}); // create signal
 // The following two methods will trigger component re-rendering
 state.a++;
 setState({a: 1000});

 // <HelloSignal ref={ref} id="some-id" />
 const HelloSignal = comp((props, ref)=>{ // Create a react component that can read the signal
 return <div>{state.a}</div>; // The dependency of the current component is state.a
 })
Enter fullscreen mode Exit fullscreen mode

Add useEffect, useLayoutEffect

The v2 version adds useEffect and useLayoutEffect two interfaces, which are also the two interfaces that this article will focus on. Why does helux provide these two interfaces to replace the original interface? Let’s see the details below.

side effects of react18

React 18 has added a heuristic concurrent rendering mechanism. The side effect function may be called multiple times due to component re-rendering. In order to help users discover possible problems caused by improper use of side effects (for example, forgetting to do cleanup), enable it in development mode In StrictMode, the side-effect function will be deliberately called twice to achieve the effect of checking the user logic, hoping that the user can correctly understand the side-effect function.

In the actual situation, under what circumstances will multiple mount behaviors occur? The new document specifically mentions an example, because in 18, react will separate the state of the component from the unloading behavior (unloading not controlled by user code), that is, the state of the component is still maintained after unmounting, and it will be restored by react when it is mounted again, for example Off-Screen Rendering scenes require this feature.

The trouble with double calls

However, this move has also brought troubles to some upgrade users. Take the following example as an example:

function UI(props: any) {
   useEffect(() => {
     console.log("mount");
     return () => {
       console.log("clean up");
     };
   }, []);
}
Enter fullscreen mode Exit fullscreen mode

In strcit mode print the following

mount
clean up
mount
Enter fullscreen mode Exit fullscreen mode

After the user actually uninstalled the component, there was still a clean up print, which made many users mistakenly thought it was a bug, and went to the react warehouse to file an issue description. After upgrading to 18, useEffect produced two calls. Later, the react official explained that this problem is normal Phenomenon, a double-call mechanism deliberately made to assist users with unreasonable side-effect functions.

However, in some scenarios, users do expect only one call during development (such as data initialization of components), so there are various ways to fight against double calls as follows.

Remove StrcitMode

The simplest and rude way is to remove the StrcitMode package at the root component to completely shield this double-call behavior.

root. render(
- <React. StrictMode>
     <App />
- </React. StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Users may only need unparalleled calls in some places, and double calls in other places to check the correctness of side effects, but this is a one-shot killing of all scene behaviors and is not very general.

Partial wrapping StrcitMode

In addition to wrapping the root component, StrcitMode also supports wrapping any subcomponent, and the user can wrap it where needed

  <React.StrictMode><YourComponent /></React.StrictMode>
Enter fullscreen mode Exit fullscreen mode

Compared with global removal, this method is milder, but wrapping StrictMode is a compulsive behavior. It needs to be exported at the code to arrange where wrapping is needed and where wrapping is not required. It is more troublesome. Is it possible to wrap StrcitMode in the root component and also What about the way to partially shield the double call mechanism? Users start from the code level, to be precise, start from the useEffect callback

Use useRef to mark the execution status

The general idea is to use useRef to record whether a side-effect function has been executed, so that the second call is ignored.

 function Demo(props: any) {
   const isCalled = React. useRef(false);

   React. useEffect(() => {
     if (isCalled. current === false) {
       await somApi.fetchData();
       isCalled. current = true;
     }
   }, []);
 }
Enter fullscreen mode Exit fullscreen mode

This has certain limitations, that is, if the dependency is added, isCalled cannot be controlled, and isCalled.current is set to false in the side effect cleaning function according to thinking, so that when the id value is changed during the lifetime of the component, mock api fetch is not printed twice despite double call behavior

  React. useEffect(() => {
     if (isCalled. current === false) {
 isCalled. current = true;
       console.log('mock api fetch');
 return ()=>{
 isCalled. current = false;
 console.log('clean up');
 };
     }
   }, [id]); // When the id changes, a new request is initiated
Enter fullscreen mode Exit fullscreen mode

But as written above, two calls still occur when the component is mounted for the first time, and the printing order is

mock api fetch
clean up
mock api fetch
Enter fullscreen mode Exit fullscreen mode

Is there a real perfect solution, so that when the StricMode is wrapped based on the root component, the side effects of the subcomponent’s initial mount and lifetime will only be called once? Next, let useEffect provided by helux solve this problem completely.

use helux's useEffect

We only need to understand the reason for the double call of react: let the component unload and separate the state, that is, the existing state of the lifetime of the component will be restored when the component is mounted again. Since there is a restoration process, the starting point is very easy Yes, the main thing is to observe the operation log at the moment when the component is restored to find the law.

First mark a sequence auto-increment id as the component example id, and observe which instance the mount behavior is for

 let insKey = 0;
 function getInsKey() {
   insKey++;
   return insKey;
 }

 function TestDoubleMount() {
   const [insKey] = useState(() => getInsKey());
   console.log(`TestDoubleMount ${insKey}`);
   React. useEffect(() => {
     console.log(`mount ${insKey}`);
     return () => console. log(`clean up ${insKey}`);
   }, [insKey]);
   return <div>TestDoubleMount</div>;
 }
Enter fullscreen mode Exit fullscreen mode

You can observe the log as shown in the figure below. It can be found that the gray print TestDoubleMount is the second call initiated by react intentionally. The side effects are all for example No. 2, and No. 1 is discarded by react as a redundant call.

image.png

Since the id is self-increasing, react will deliberately initiate two calls to the same component, discard the first and repeat the side effects for the second call (mount-->clean-->mount ---> after the component is unloaded clean), then we only need to check whether there is a side effect record in the previous example when the second side effect is executed, and record the execution times of the second side effect at the same time, it is easy to shield the side effect of the second mode, that is (mount -->clean-->mount ---> clean after component uninstallation) is changed to (mount ---> clean after component uninstallation), and execute the set clean when the component is actually uninstalled.

The pseudo code is as follows

function mayExecuteCb(insKey: number, cb: EffectCb) {
   markKeyMount(insKey); // Record the number of times the current instance id is mounted
   const curKeyMount = getKeyMount(insKey); // Get the current instance mount information
   const pervKeyMount = getKeyMount(insKey - 1); // Get the mount information of the previous instance
   if (!pervKeyMount) { // The previous example has no mount information, which is a double call behavior
     if (curKeyMount && curKeyMount.count > 1) { // The user's side effect function is being executed only when the current instance is mounted for the second time
       const cleanUp = cb();
       return () => {
         clearKeyMount(insKey); // Clear the mount information of the current instance
         cleanUp && cleanUp(); // return cleanup function
       };
     }
   }
}
Enter fullscreen mode Exit fullscreen mode

On this basis, encapsulating a useEffect to the user can achieve the purpose we mentioned above: when wrapping StricMode based on the root component, the sub-component's initial mount and lifetime side effects only occur once.

 function useEffect(cb: EffectCb, deps?: any[]) {
   const [insKey] = useState(() => getInsKey()); // Write as a function to avoid key auto-increment overhead
   React. useEffect(() => {
     return mayExecuteCb(insKey, cb);
   }, deps);
 }
Enter fullscreen mode Exit fullscreen mode

If you are interested in the specific implementation of useEffect, you can check the warehousecode

Now you can use the useEffect exported by helux just like using the native useEffect, and enjoy the benefit of not requiring double-call detection in some scenarios.

import { useEffect } from 'helux';

useEffect(() => {
   console.log('mock api fetch', id);
   return () => {
     console.log('mock clean up');
   };
}, [id]); // When the id changes, a new request is initiated
Enter fullscreen mode Exit fullscreen mode

Conclusion

Understanding the design intention and process of double call will help us understand more clearly how side-effect functions are managed, and it can also help us make better decisions to avoid the double call mechanism.

helux belongs to the module federation sdk hel-micro subpackage, original intention It is to provide a more flexible and low-cost state sharing method for react. If you are interested in helux or hel-micro, please pay attention and give us more feedback for improvement.

💖 💪 🙅 🚩
fantasticsoul
幻魂

Posted on May 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related