Helux 2 released to solve the side effect double call mechanism of react18
Tags: react
Published: 2023-05-30 05:47:00

> 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](https://github.com/tnfe/hel/tree/main/packages/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](https://codesandbox.io /s/helux-effect-qyv6xz?file=/src/App.tsx) to learn more.

``` diff
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>;
}
```

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

```ts
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});
```

## 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...
     }

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](https://www.npmjs. com/package/@preact/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
     })

### 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:

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

In strcit mode print the following

```txt
mount
clean up
mount
```

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.

``` diff
root. render(
- <React. StrictMode>
     <App />
- </React. StrictMode>
);
```

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

```tsx
  <React.StrictMode><YourComponent /></React.StrictMode>
```

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;
         }
       }, []);
     }

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

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

```txt
mock api fetch
clean up
mock api fetch
```

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>;
     }

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](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8a615694b11f479b9dd692940d6aeced~tplv-k3u1fbpfcp-watermark.image?)

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

```ts
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
       };
     }
   }
}
```

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);
     }

If you are interested in the specific implementation of `useEffect`, you can check the warehouse[code](https://github.com/tnfe/hel/blob/main/packages/helux/src/hooks/useEffect.ts)

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.

```ts
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
```

### 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](https://github.com/tnfe/hel/blob/main/packages/helux) belongs to the module federation sdk [hel-micro](https://github.com/tnfe/hel) 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.