React 18 adds 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 development mode, side effects will be deliberately called twice. function to achieve the effect of checking user logic, but this has also caused trouble to some upgraded users. This article will discuss how helux avoids 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 be replaced useState
with useShared
, and then you can improve the react local state to a global shared state without modifying a single line of other code. Effect, 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 relies 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, and users are expected to change the state in a react-like manner. If the user sets it 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 that use the `sharedObj.a` value sharedObj. a ++; setState ({ a : 1000 });
What 2.0 brings
2.0
The version has made the following three adjustments:
Simplify API naming
The original useSharedObject
API has been re-exported into a more streamlined one useShared
to createShared
improve the user’s writing efficiency and reading experience.
Added new signal record (experimental)
helux-signal
Signal-related record data has been added internally to prepare the relevant infrastructure for future releases (a react signal pattern implementation library based on helux encapsulation) helux-signal
. It is still in the prototype stage and will release a version experience at the appropriate time beta
.
When not using signals, you need createShared
and useShared
to work together createShared
to create a shared state and useShared
be responsible for consuming the shared state. It returns a specific readable state value and update function.
const { state : sharedObj } = createShared({a: 100 , b: 2 }); // Create function HelloHelux(props: any) { const [state, setState] = useShared(sharedObj); // 使用 // render logic ... }
When using signals, you only need to call helux-signal
an interface createSignal
to complete the creation of the state, and then the component can skip useShared
the hook function and read the shared state directly.
It should be noted that at present, helux 2 has only completed the relevant infrastructure internally. The upper layer is responsible for the specific implementation of helux-signal, which is still in the experimental stage. At this stage, the community already has the signals-react library provided by preact to support the development of react components in signal mode.
An example of a complete expected helux-signal
development of react-based components is as follows:
import { createSignal, comp } from 'helux-signal'; const { state , set State } = createSignal({a: 100 , b: 2 }); // Create signal // The following two methods will trigger component re-rendering state .a++; set State({a: 1000 }); // <HelloSignal ref={ref} id="some-id" /> const HelloSignal = comp((props, ref)=>{ // Create a react component that can read signals return <div> { state .a}</div>; // The dependency of the current component is state .a })
Added useEffect, useLayoutEffect
The v2 version adds useEffect
two useLayoutEffect
new interfaces, which are also the two interfaces that this article will focus on. Why are helux
these two interfaces provided to replace the native interfaces? Let’s look at the following content for detailed explanation.
side effects of react18
React 18 adds 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 incorrect use of side effects (such as forgetting to do cleanup behavior), it is enabled in development mode. In StrictMode, the side effect function will be deliberately called twice to achieve the effect of checking the user’s logic, and it is expected that the user will correctly understand the side effect function.
What actual situation will cause multiple mounting behaviors? The new document specifically mentions an example. Since in React 18, React will separate the component’s state and uninstallation behavior (uninstallation controlled by non-user code), that is, the component will remain in its uninstalled state, and will be restored internally by React when it is mounted again. For example This feature is required for off-screen rendering scenes.
The trouble with double calls
However, this move has also caused trouble for some upgrade users. Take the following example as an example:
function UI(props: any) { useEffect(() => { console.log("mount"); return () => { console.log("clean up"); }; }, []); }
In strcit mode print as follows
mount clean up mount
After the user actually uninstalled the component, there was another clean up
printing, which made many users mistakenly think it was a bug. They went to the react warehouse to file an issue and described that useEffect was called twice after upgrading to 18. Later, react officials specifically explained that this problem was a normal phenomenon and provided assistance. The user has an unreasonable side-effect function and a deliberate double-calling mechanism.
However, in some scenarios, users really expect that only one call will be made during development (such as component data initialization), so there are various ways to combat double calls as follows.
RemoveStrcitMode
The simplest and crudest way is to remove the package at the root component StrcitMode
to completely block this double calling behavior.
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 kill for all scene behaviors and is not very general.
Partially wrapped StrcitMode
StrcitMode
In addition to wrapping the root component, it also supports wrapping any sub-component. Users can wrap it wherever needed.
<React.StrictMode><YourComponent /></React.StrictMode>
Compared with global removal, this method is gentler, but wrapping StrictMode is a forced behavior. It needs to be exported from the code to arrange where it needs to be wrapped and where it does not need to be wrapped. It is more troublesome. Is there a way to wrap the root component StrcitMode
and partially shield the double What about the way to call the mechanism? Users started to start from the code level, to be precise, they useEffect
started from the callback.
Use useRef to mark execution status
The general idea is to use useRef
status 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 dependencies are added, isCalled
it cannot be controlled. According to thinking, the side effects of the cleanup function will be set isCalled.current
to false. In this way, when the id value is changed during the lifetime of the component, even if there are double calls, it will not Print twicemock api fetch
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, initiate a new request
However, 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
Is there a truly perfect solution so that StricMode
when wrapping based on the root component, the sub-component will only be called once for initial mounting and lifetime side effects? Next, let us helux
provide you useEffect
with a complete solution to this problem.
Use helux’s useEffect
We only need to core understand the reason for the double call of react: to separate the component uninstallation and state, that is, when the component is mounted again, the existing state of the existence period will be restored . Since there is a restoration process, the starting point is very easy, mainly Observe the running log at the moment the component is restored to look for patterns.
First mark a sequence of auto-incrementing ids as the component example id, and observe which instance the mounting 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 below. You can find that the gray print TestDoubleMount
is the second call intentionally initiated by react. The side effects are all for example No. 2. No. 1 is discarded by react as a redundant call.
Since the id is auto-incremented, react will deliberately initiate two calls to the same component, discard the first one and repeat the side effects for the second call (mount–>clean–>mount —> after the component is uninstalled clean), then when the second side effect is executed, we only need to check whether there is a side effect record in the previous example, and record the number of executions of the second side effect. It is easy to shield the side effects of the second mode, that is (mount –>clean–>mount —> clean after component uninstallation) has been modified to (mount —> clean after component uninstallation), and the set clean is executed when the component is actually uninstalled.
The pseudo code is as follows
function mayExecuteCb ( insKey: number , cb: EffectCb ) { markKeyMount (insKey); // Record the current instance id and the number of mounts 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) { // If the previous example has no mount information, it is a double call behavior if (curKeyMount && curKeyMount. count > 1 ) { // The second time of the current instance Mounting is executing the user's side effect function const cleanUp = cb (); return () => { clearKeyMount (insKey); // Clean up the current instance mounting information cleanUp && cleanUp (); // Return the cleanup function }; } } }
On this basis, encapsulating one useEffect
for the user can achieve the purpose we mentioned above: StricMode
when wrapping based on the root component, the sub-component’s initial mounting and lifetime side effects will only occur once.
function useEffect(cb: EffectCb, deps?: any[]) { const [ insKey ] = use State(() => get InsKey() ); // Write as a function to avoid key increment overhead React . use Effect(() => { return mayExecuteCb(insKey, cb); }, deps); }
If you are interested in useEffect
the specific implementation, you can view the warehouse code
useEffect
Now you can use the helux
exported one just like the native one useEffect
, while enjoying 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, initiate a new request