The original link
preface
This article analyzes the advantages and disadvantages of React Hook apis from the perspective of Mini React — Preact source code. To understand why hooks are used and how best to use them.
Article 2 the rules
Why is that?
- ✅ use hooks only at the top level and do not call hooks in loops, conditions, or nested functions;
- ✅ only call hooks in React functions, not in normal JavaScript functions.
Source code analysis
let currentIndex; // Global index
let currentComponent; // The component where the current hook is located
function getHookState(index) {
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = {_list: []._pendingEffects: []});
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
Copy the code
// Get the registered hook
const hookState = getHookState(currentIndex++);
Copy the code
- Hook states are maintained in an array structure, executed
hook api
When the indexcurrentIndex + 1
Put them in arrays one by one. When the componentrender
Before, it callshook render
, reset the index and set the current component, hook injection inoptions
Inside.
options._render = vnode= > {
currentComponent = vnode._component;
currentIndex = 0;
// ...
};
Copy the code
-
The first thing to remember is that a function component reexecutes the entire function every time it diff, whereas a class component only executes this.render. Therefore, hooks suffer a performance loss. Provides useMemo and useCallback optimizations.
-
Hook in each render, the last hook state, if executed in the loop, conditional or nested function of the uncertain branch, it is possible to fetch wrong data, resulting in chaos.
function Todo(props) {
const [a] = useState(1);
if(props.flag) {
const [b] = useState(2);
}
const [c] = useState(3);
// ...
}
Copy the code
<Todo flag={true} / >Copy the code
- At this time
a = 1, b = 2, c = 3
;
<Todo flag={false} / >Copy the code
- When the conditions are changed,
a = 1, c = 2
。c
Wrong state!
The react component and its lifecycle are parasitic on hooks.
Preact hook
在options
Object_render
->diffed
->_commit
->unmount
Four hooks, each executed before the life cycle of the object component, are less intrusive.
useState
use
/ / declare the hooks
const [state, setState] = useState(initialState);
/ / update the state
setState(newState);
// Functional updates are also available
setState(prevState= > { // Get the last state value
// You can also use object.assign
return{... prevState, ... updatedValues}; });Copy the code
- Lazy initial state. If you initialize
state
Values are expensive, functions can be passed in, and initialization is performed only once.
const [state, setState] = useState((a)= > {
const initialState = someExpensiveComputation(props);
return initialState;
});
Copy the code
- Skip state updates. Set the same value (
Object.is
Determine), does not trigger component updates.
const [state, setState] = useState(0);
// ...
// Updating state does not trigger component re-rendering
setState(0);
setState(0);
Copy the code
Why is that?
- Pit: rely on
props.state === 1
Initialize thehook
Why,props.state === 2
When,hook state
No change?
function Component(props) {
const [state, setState] = useState(props.state);
// ...
}
Copy the code
- What is the principle of lazy initialization?
hook state
How do changes drive component rendering, and why are they acceptableclass state
Use?
Source code analysis
Preact
中useState
Is the use ofuseReducer
Implementation, easy to write, the code will be slightly modified.
function useState(initialState) {
const hookState = getHookState(currentIndex++);
if(! hookState._component) { hookState._component = currentComponent; hookState._value = [ invokeOrReturn(undefined, initialState),
action => {
const nextValue = invokeOrReturn(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue; hookState._component.setState({}); }}]; }return hookState._value;
}
Copy the code
// Utility functions to support functional initialization and updating
function invokeOrReturn(arg, f) {
return typeof f === 'function' ? f(arg) : f;
}
Copy the code
- It can be seen that
useState
Only the first time in the componentrender
Is initialized once, and the status is later updated by the returned function.
- Pit: Initializations (including passed functions) are performed only once and should not be relied upon
props
To initializeuseState
; - Optimization: You can use passed in functions to optimize performance for expensive initialization operations.
hookState._value[0] ! == nextValue
Compare old and new values to avoid unnecessary rendering.- As you can see, the update operation takes advantage of the component instance’s
this.setState
Function. That’s whyhook
Can replaceclass
的this.state
Use.
useEffect
use
- For example, the common basis
query
If the component is loaded for the first time, only one request is sent.
function Component(props) {
const [state, setState] = useState({});
useEffect((a)= > {
ajax.then(data= >setState(data)); } []);/ / dependencies
// ...
}
Copy the code
useState
Have said,props
The initialstate
There are pits. You can use themuseEffect
The implementation.
function Component(props) {
const [state, setState] = useState(props.state);
useEffect((a)= > {
setState(props.state);
}, [props.state]); // props. State changes the value to state
// ...
}
Copy the code
- Clear side effects, such as listening to resize the browser window, and then clear side effects
function WindowWidth(props) {
const [width, setWidth] = useState(0);
function onResize() {
setWidth(window.innerWidth);
}
// Perform side effects only once and the component will be cleared when unmounted
useEffect((a)= > {
window.addEventListener('resize', onResize);
return (a)= > window.removeEventListener('resize', onResize); } []);return <div>Window width: {width}</div>;
}
Copy the code
- Note: in
useEffect
In the use ofstate
It is best to rely on it, otherwise it is easy to producebug
function Component() {
const [a, setA] = useState(0);
useEffect((a)= > {
const timer = setInterval((a)= > console.log(a), 100);
return (a)= > clearInterval(timer)
}, []);
return <button onClick={()= > setA(a+1)}>{a}</button>
}
Copy the code
When you click the button A +=1, console.log still prints 0. This is because the useEffect side effect will only be the _pendingEffects array when the component is first loaded, forming a closure.
Modified as follows:
function Component() {
const [a, setA] = useState(0);
useEffect(() => {
const timer = setInterval(() => console.log(a), 100);
return () => clearInterval(timer)
-} []);
+ }, [a]);
return <button onClick={() => setA(a+1)}>{a}</button>
}
Copy the code
This code runs in React, and the output changes when a button is clicked. In Preact, the timer is not cleared, indicating a bug. -_ – | |
Why is that?
useEffect
What problem was solved
Generally send data requests to componentDidMount, after which componentWillUnmount is cleaned up in relation. This results in unrelated logic being intermingled with componentDidMount, while the corresponding cleanup work is assigned to componentWillUnmount.
With useEffect, you can write independent logic in different Useeffects without having to worry about cleaning up other blocks of code while worrying about maintenance.
- Is performing side effects (changing the DOM, adding subscriptions, setting timers, logging, etc.) inside a component function not allowed?
Every time the diff function component is used like the this.render function of the class component, the whole is executed, and the side effects of operating in the body are fatal.
useEffect
The mechanism?
Source code analysis
function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if(argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent.__hooks._pendingEffects.push(state); }}Copy the code
- Utility functions with a dependency of
undefined
Or a value in the dependency array changes, thentrue
function argsChanged(oldArgs, newArgs) {
return! oldArgs || newArgs.some((arg, index) = >arg ! == oldArgs[index]); }Copy the code
- The callback function that shows the side effects will be in
_pendingEffects
Array maintenance, code is executed in two places
options._render = vnode= > {
currentComponent = vnode._component;
currentIndex = 0;
if (currentComponent.__hooks) { // Why do you need to clean it up?!currentComponent.__hooks._pendingEffects.forEach(invokeCleanup); currentComponent.__hooks._pendingEffects.forEach(invokeEffect); currentComponent.__hooks._pendingEffects = []; }};Copy the code
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
function invokeEffect(hook) {
const result = hook._value(); // If a side effect function returns a function, it is saved as a cleanup function.
if (typeof result === 'function') hook._cleanup = result;
}
Copy the code
options.diffed = vnode= > {
const c = vnode._component;
if(! c)return;
const hooks = c.__hooks;
if (hooks) {
if(hooks._pendingEffects.length) { afterPaint(afterPaintEffects.push(c)); }}};Copy the code
function afterPaint(newQueueLength) {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;
(prevRaf || afterNextFrame)(flushAfterPaintEffects);
}
}
Copy the code
function flushAfterPaintEffects() {
afterPaintEffects.some(component= > {
if (component._parentDom) {
try {
component.__hooks._pendingEffects.forEach(invokeCleanup);
component.__hooks._pendingEffects.forEach(invokeEffect);
component.__hooks._pendingEffects = [];
} catch (e) {
options._catchError(e, component._vnode);
return true; }}}); afterPaintEffects = []; }Copy the code
-
I suspect that options._render code is copied from flushafterpaint ects without thinking about it. Resulting in a bug mentioned above.
-
AfterPaint uses requestAnimationFrame or setTimeout for the following purposes
Unlike componentDidMount and componentDidUpdate, the function passed to useEffect is delayed after the browser has laid out and drawn, and does not block the browser update screen. (erratum: useEffect in React does this, Preact does not)
useMemo
use
function Counter () {
const [count, setCount] = useState(0);
const [val, setValue] = useState(' ');
const expensive = useMemo((a)= > {
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum
}, [ count ]); // ✅ The callback is executed only if the count changes
return (
<>
<span>You Clicked {expensive} times</span>
<button onClick={() => setCount(count + 1)}>Click me</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</>
)
}
Copy the code
Why is that?
useMemo
What problem was solved
As stated above, function components should be executed repeatedly, which can cost performance if done at a high cost. React provides useMemo to cache the results of function execution and useCallback to cache functions.
Source code analysis
function useMemo(factory, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._args = args;
state._factory = factory;
return (state._value = factory());
}
return state._value;
}
Copy the code
-
As you can see, the function passed in is simply executed according to the dependency and the result is stored in the internal hook state.
-
Remember that all hook apis are the same, do not use state in side leases without passing it in as a dependency.
useCallback
use
const onClick = useCallback(
(a)= > console.log(a, b),
[a, b]
);
Copy the code
Why is that?
useCallback
What problem was solved
As mentioned above, it’s used to cache functions
- For example, the example above optimizes the listening window.
function WindowWidth(props) {
const [width, setWidth] = useState(0);
- function onResize() {
- setWidth(window.innerWidth);
-}
+ const onResize = useCallback(() => {
+ setWidth(window.innerWidth);
+} []);useEffect(() => { window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); } []); return <div>Window width: {width}</div>; }Copy the code
As mentioned above, if you don’t have dependencies, you don’t have to use width, but you can use setWidth, the function is a reference, and the closure variable setWidth is the same address.
Source code analysis
useMemo
The encapsulation
function useCallback(callback, args) {
return useMemo((a)= > callback, args);
}
Copy the code
useRef
use
- For example, click the button to start the 60-second countdown and click again to stop it.
function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
useEffect((a)= > { // effect function that does not take or return any arguments
let interval;
if (start) {
interval = setInterval((a)= > {
setTime(time - 1); // ❌ time is not available in effect closures
}, 1000);
}
return (a)= > clearInterval(interval) // The clean-up function, called when the current component is deregistered
}, [start]); // The effect function is called when the variables in the array change
return (
<button onClick={()= >setStart(! start)}>{time}</button>
);
}
Copy the code
- In the previous analysis, because of the closure, get to
time
The value is not up to date. You can usetime
To the initial value ofuseRef
, and then drivetime
The update.
function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
+ const currentTime = useRef(time); // Generate a mutable referenceUseEffect (() => {// effect function does not accept or return any parameters let interval; if (start) { interval = setInterval(() => {+ setTime(currentTime.current--) // CurrentTime.current is mutable
- setTime(time - 1); // ❌ time is not available in effect closures}, 1000); } return () => clearInterval(interval) // clean-up function, called when the current component is deregistered}, [start]); Return (<button onClick={() => setStart(! start)}>{time}</button> ); }Copy the code
-
UseRef generates an object currentTime = {current: 60}, which remains constant throughout the life of the component.
-
SetTime can be used to replace interval, so that external countdowns can also be cancelled.
function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
- const currentTime = useRef(time); // Generate a mutable reference
+ const interval = useRef() // Interval can be cleared and set anywhere in this scopeUseEffect (() => {// effect takes no arguments and returns no arguments- let interval;
if (start) {
- interval = setInterval(() => {
+ interval.current = setInterval(() => {
-settime (currentTime.current--) // currentTime.current is mutable
+ setTime(t => t-1) // ✅
}, 1000);
}
- return () => clearInterval(interval) // The clean-up function, which is invoked when the current component is deregistered
+ return () => clearInterval(interval.current) // Clean-up function, which is called when the current component is deregistered}, [start]); Return (<button onClick={() => setStart(! start)}>{time}</button> ); }Copy the code
This eliminates repeated creation of interval variables and allows outsiders to clean up the timer interval.current.
Why is that?
useRef
The returned object remains unchanged for the lifetime of the component.- Why can’t you change the returned object, but only the object
current
Attribute?
Source code analysis
function useRef(initialValue) {
return useMemo((a)= > ({ current: initialValue }), []);
}
Copy the code
- Internally used
useMemo
To implement, pass in one to generate one withcurrent
Property object function, null array dependent, so the function is executed only once during the entire life cycle. - Direct change
useRef
The value returned cannot be changed internallyhookState._value
Values can only be changed internallyhookState._value.current
To influence the next use.
useLayoutEffect
use
- with
useEffect
Use the same way.
Why is that?
- with
useEffect
What’s the difference?
Source code analysis
-
UseEffect callbacks are executed asynchronously in the option.diffed phase using requestAnimationFrame or setTimeout(callback, 100). Since the authors agree that this is not such a big deal, the code is not posted and there is only one layer of requestAnimationFrame that will not be executed before the next frame.
-
The useLayoutEffect callback is batch synchronized in the option._commit phase.
-
In React, requestIdleCallback or requestAnimationFrame is estimated to be used for time sharding to avoid blocking visual updates.
-
React uses its own internal priority scheduler, which will cause some low-priority tasks to be delayed. You can use useLayoutEffect if you feel that the priority is too high to be synchronized regardless of blocking the render.
useReducer
use
- Numbers ±1 with reset
const initialState = 0;
const reducer = (state, action) = > {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action'); }};function Counter() {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={()= > dispatch('increment')}>+1</button>
<button onClick={()= > dispatch('decrement')}>-1</button>
<button onClick={()= > dispatch('reset')}>reset</button>
</div>
);
}
Copy the code
- The second argument can be a function that returns
state
Initial value of; - The third argument can be returned by a function that takes the second argument as an argument
state
The initial value of.
Why is that?
- When to use
useReducer
?
The state logic is complex and contains multiple subvalues, and the next state depends on the previous state.
Reducer had better be a pure function, centralized processing logic, modify the source to facilitate traceability, avoid logic scattered, can also avoid unpredictable changes in the state, resulting in difficult bug traceability.
Source code analysis
- As I said,
useState
是useReducer
The implementation.
function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++);
if(! hookState._component) { hookState._component = currentComponent; hookState._value = [ !init ? invokeOrReturn(undefined, initialState) : init(initialState),
action => {
const nextValue = reducer(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue; hookState._component.setState({}); }}]; }return hookState._value;
}
Copy the code
- The last time
state
为reducer
The first argument to,dispatch
Accepts the second argument and generates a new onestate
.
useContext
use
- For example, set the global theme
theme
// App.js
function App() {
return<Toolbar theme="dark" />; } // toolanto.js function Toolbar(props) {// Theme needs to be layered around all the components. return ( <div> <ThemedButton theme={props.theme} /> </div> ); } // ThemedButton.js class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; }}Copy the code
- Use the Context
// context.js
+ const ThemeContext = React.createContext('light');
// App.js
function App() {
- return
;
+ return (
+
+
+
);
}
// Toolbar.js
function Toolbar(props) {
return (
<div>
-
+
// no passing required
</div>
);
}
// ThemedButton.js
class ThemedButton extends React.Component {
+ static contextType = ThemeContext; // Specify contextType to read the current theme context.
render() {
- return ;
+ return ; // React will find the nearest theme Provider, whose theme value is "dark".}}Copy the code
- Using useContext
// context.js const ThemeContext = React.createContext('light'); // App.js function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } // toolanto.js function (props) {return (<div> <ThemedButton /> // no need to pass </div>); } // ThemedButton.js- class ThemedButton extends React.Component {
- static contextType = ThemeContext; // Specify contextType to read the current theme context.
- render() {
- return ; // React will find the nearest theme Provider, whose theme value is "dark".
-}
-}
+ function ThemedButton() {
+ const theme = useContext(ThemeContext);
+
+ return ;
+}
Copy the code
-
UseContext (MyContext) is equivalent to static contextType = MyContext in the class component
-
When the most recent <MyContext.Provider> update is made to the component’s upper layer, the Hook triggers a rerender and uses the latest context value passed to MyContext Provider. Even if the ancestor uses React. Memo or shouldComponentUpdate, it will be rerendered when the component itself uses useContext.
You can use React. Memo or useMemo hooks for performance optimization.
Why is that?
- How does useContext get the context and drive the change?
Source code analysis
- On the current component, get the context, subscribe to the current component, and publish notifications when the context changes.
function useContext(context) {
const provider = currentComponent.context[context._id];
if(! provider)return context._defaultValue;
const state = getHookState(currentIndex++);
// This is probably not safe to convert to "!"
if (state._value == null) {
state._value = true;
provider.sub(currentComponent);
}
return provider.props.value;
}
Copy the code
Customize the hook
use
- Common to add anti-shake features to components, such as using ANTD
Select
或Input
Components, you may use them separately to reassemble a new component, with the anti-shake implementation inside the new component. - Custom hook can be used to separate the relationship between components and anti-shake in finer granularity.
/ / if the hooks
function useDebounce() {
const time = useRef({lastTime: Date.now()});
return (callback, ms) = > {
time.current.timer && clearTimeout(time.current.timer);
time.current.timer = setTimeout((a)= > {
const now = Date.now();
console.log(now - time.current.lastTime); time.current.lastTime = now; callback(); }, ms); }}Copy the code
function App() {
const [val, setVal] = useState();
const inputChange = useDebounce();
// Can be used multiple times
// const selectChange = useDebounce();
return (
<>
<input onChange={
({target: {value}}) => {
inputChange(() => setVal(value), 500)
}
}/>{val}
</>
);
}
Copy the code
Function component hook versus class component
disadvantages
- Poor performance, but only at the expense of browser parsing at the JS level.
advantage
- Reduce the amount of code, related logic more aggregation, easy to read and maintain;
- Don’t understand
class
和this
.class
So far it’s just syntactic sugar, standards are still changing, there’s no concept like traditional object-oriented polymorphism, multiple inheritance,this
The cost of understanding is high; - Pure functions are good for example
ts
Derivation types, and so on.
reference
- The React document
- Preact document
- Preact source
- Refactor your applet using React Hooks