A Hook rules
Use hooks only at the top level
Do not call hooks in loops, conditions, or nested functions. Be sure to always call React functions at the top level and before any returns. Following this rule ensures that hooks are called in the same order on every render.
Why is order guaranteed? React uses the sequence of Hook calls to determine the relationship between state and useState
function Form() {
const [name, setName] = useState('Mary');
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
const [surname, setSurname] = useState('Poppins');
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
Copy the code
// ------------
// First render
// ------------
useState('Mary') // 1. Initialize state of variable name with 'Mary'
useEffect(persistForm) // 2. Add effect to save form action
useState('Poppins') // 3. Initialize the state of the surname variable with 'Poppins'
useEffect(updateTitle) // 4. Add effect to update the title
// -------------
// Secondary render
// -------------
useState('Mary') // 1. Read state with variable name (argument ignored)
useEffect(persistForm) // 2. Replace save form effect
useState('Poppins') // 3. Read the state of a variable named surname (parameter ignored)
useEffect(updateTitle) // 4. Replace the effect that updates the title
// ...
Copy the code
In the code above, React correctly associates the internal state with the corresponding Hook as long as the order of Hook calls is consistent across multiple renders.
if(name ! = =' ') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
Copy the code
The code above is named! If this condition is true, we will useEffect the Hook. But the next time we render, we might do something that changes the expression value to false. In this case, the rendering will skip the Hook, and the order of Hook calls has changed as follows
useState('Mary') // 1. Read state with variable name (argument ignored)
// useEffect(persistForm) // 🔴
useState('Poppins') // 🔴 2 (previously 3). Failed to read state of surname variable
useEffect(updateTitle) // 🔴 3 (previously 4). Failed to replace effect updating title
Copy the code
React does not know what the second useState Hook should return. React would think that the call to the second Hook in this component would correspond to the effect persistForm like the last render, but it does not. From here, subsequent Hook calls are executed ahead of time, resulting in bugs.
Rules for using hooks: 1. Do not call Hook 2 in normal JavaScript functions. Use the React function component to call a Hook. 3. Use the custom Hook
The Capture feature
1.Class Component VS Function Component
Compare the Function Component code
function Counter() {
const [count, setCount] = useState(0);
const log = () = > {
setCount(count + 1);
setTimeout(() = > {
console.log(count); / / 0 1 2
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
Class Component
class Counter extends Component {
state = { count: 0 };
log = () = > {
this.setState({
count: this.state.count + 1
});
setTimeout(() = > {
console.log(this.state.count); / / 3 3 3
}, 3000);
};
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.log}>Click me</button>
</div>); }}Copy the code
The Function of the above code is that after clicking the button, the count value will be printed in 3s. In 3s, click button3 times continuously, the result of Class and Function is completely different
Class Component First state is Immutable; setState must generate a new state reference. But the Class Component reads state in this.state, which results in the latest reference to state every time the code executes, so three quick clicks result in 3, 3, 3.
Function Component useState generates data Immutable, and by passing a new value to the second parameter Set, the original value will form a new reference for the next rendering. However, since state is not read through this., each setTimeout reads the data of the current render closure environment. Although the latest value changes with the latest render, the state of the old render is still the old value.
Each render is a separate closure, and the value of count in each of the three separate renders is 0, 1, 2, so no matter how long setTimeout is delayed, the printed result is always 0, 1, 2.
The above code uses the useState Hook, which is described below
2.useState
What it does: Returns a state, and the state update function setState, which receives a new state value and causes the component to re-render.
Use: 1. During rendering, the state returned is the same value as the first parameter passed in. 2. If the new state needs to be computed using the previous state, then the function can be passed to setState. This function takes the previous state and returns an updated value. In the following example, the prevCount value is always computed from the initial count
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={()= > setCount(initialCount)}>Reset</button>
<button onClick={()= > setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={()= > setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
Copy the code
3. A current practice in useState is to write variable names flat rather than in a State object like the Class Component, as shown in the following code
function FunctionComponent {
const [left,setLeft] = useState(0)
const [top,setTop] = useState(0)
const [width,setWidth] = useState(100)
const [height,setHeight] = useState(100)}Copy the code
class ClassComponent extends React.PureComponent {
state = {
left: 0.top: 0.width: 100.height: 100
};
}
Copy the code
In fact, you can aggregate State in Function Component, but instead of automatically merging, you need to use extension operators… The state of grammar
function FunctionComponent() {
const [state, setState] = useState({
left: 0.top: 0.width: 100.height: 100
});
}
Copy the code
setState(state= > ({ ...state, left: e.pageX, top: e.pageY }));
Copy the code
Note: 1. If your update function returns exactly the same value as the current state, subsequent rerenders will be skipped entirely.
2. The initialState parameter is only used in the initial rendering of the component and will be ignored in subsequent renderings.
3. If the initial state needs to be obtained through complex calculations, you can pass in a function that calculates and returns the initial state. Since the parameters of useState function are initial values, but the entire function is Render, so every initialization will be called, only the initial parameters are ignored, if the initial value calculation is very time-consuming, it is recommended to use the function passed in, so that it will only be executed once
const [state, setState] = useState(() = > {
const initialState = someExpensiveComputation(props);
return initialState;
});
Copy the code
How do I circumvent the Capture feature?
The first method is useRef
function Counter() {
const count = useRef(0);
const log = () = > {
count.current++;
setTimeout(() = > {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
As in the code above, the print result is 3, 3, 3. The Hool useRef was used above. Let’s take a look
1.useRef
UseRef returns a mutable ref object whose current property is initialized as the passed parameter. The returned REF object persists throughout the re-rendering of the component. The difference from useState is that objects created by useState are independent within each re-rendering process while ueseRef is shared
So in the above code, assigning or reading count.current always reads its most recent value, regardless of the render closure, so if you do three quick clicks, you must return 3, 3, 3.
The problem with this scheme is that useRef creates values instead of useState, so the natural question is how to achieve the same effect without changing the way the original values are written.
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() = > {
currentCount.current = count;
}, [count]);
const log = () = > {
setCount(count + 1);
setTimeout(() = > {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
As in the above code, you can define a state and use the state value as the initial value of useRef to update the useRef value in useEffect.
The useEffect is used above, which is introduced below
2.useEffect
What it does: This Hook receives a function that contains imperative code that may have side effects.
Side effects: Changing the DOM, adding subscriptions, setting timers, logging, and performing other side effects within the function component body (in this case, during the React rendering phase).
Timing: useEffect will be executed at the end of each rendering round, but you can choose to have it executed only when dependency changes are made.
When a component is destroyed, the timer defined by useEffect needs to be destroyed, which can be done with the return value of useEffect, as shown in the following code
useEffect(() = > {
const id = setInterval(() = > {
setCount(c= > c + 1);
}, 1000);
return () = > clearInterval(id); } []);Copy the code
What happens if you replace setTimeout with setInterval?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() = > {
const id = setInterval(() = > {
setCount(count + 1);
}, 1000);
return () = > clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code
Because useEffect’s dependency is an empty array, it only runs the function passed in once during the entire rendering cycle, so setInterval stays in the count = 0 closure, resulting in count being 1 after each set
Add the useEffect dependency to the functionality of the code and it will work
function Counter() {
const [count, setCount] = useState(0);
useEffect(() = > {
const id = setInterval(() = > {
setCount(count + 1);
}, 1000);
return () = > clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
Copy the code
One problem with this is that each time the counter is re-instantiated and destroyed, the performance cost increases
How to solve this problem?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() = > {
const id = setInterval(() = > {
setCount(c= > c + 1);
}, 1000);
return () = > clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code
Using the second use of useState assignment, which does not rely directly on count but instead uses a function callback, has the benefit of 1. Don’t rely on count. 2. The dependency is [], and only initialization will instantiate setInterval. And the reason the output is still correct is 1, 2, 3… The reason is that in setCount’s callback function, the c value always points to the latest count value, so there is no logical loophole.
If two state variables are used at the same time, this method will fail. How to solve this problem?
3.useReducer
const [state, dispatch] = useReducer(reducer, initialState);
Copy the code
The structure returned by useReducer is similar to that of useState except that the second item in the array is dispatch and two parameters are received, the first is reducer and the second is the initial value. Reducer defines how to transform data. You can call Dispatch to implement reducer
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() = > {
const id = setInterval(() = > {
dispatch({ type: "tick" });
}, 1000);
return () = > clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
function reducer(state, action) {
switch (action.type) {
case "tick":
return {
...state,
count: state.count + state.step }; }}Copy the code
It can be seen that the addition of count was completed through the reducer tick type, while in the useEffect function, count and step variables were completely bypassed.
This example does have one dependency, which is Dispatch, but the Dispatch reference never changes, so you can ignore it.
4. UseMemo and useCallback(calculated attributes)
What it does: Returns a cached variable/function what it does: Helps avoid costly calculations on every render
Execution timing: recalculation occurs when a dependency changes, only once if an empty array is passed in [], and the new value is evaluated on each render if no dependency array is provided.
Note: The function passed in is executed during rendering. Do not perform non-rendering operations inside this function. Operations such as side effects are the domain of useEffect, not useMemo and useCallback.
Use useMemo for more detailed optimization
const Child = (props) = > {
useEffect(() = > {
// ...
}, [props.name])
return useMemo(() = > (
<h1>{props.name}</h1>
), [props.name])
}
Copy the code
This uses useMemo to wrap the rendering code so that even if the Child function is re-executed because of the props change, it will not re-render as long as the props. Name is not changed.
The following is a classic example of a parent component that “pits” its children in development
/ / the parent component
function App() {
const [count, forceUpdate] = useState(0);
const daphnisli = { name: 'li3 shuang3' };
return (
<div>
<Child daphnisli={daphnisli} />
<div onClick={()= > forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
/ / child component
const Child = (props= > {
useEffect(() = > {
console.log("daphnisli", props.name);
}, [props.name]);
return <div>Child</div>;
});
Copy the code
The log is printed whenever the parent props. Daphnisli changes. The result is that every time the parent component refreshes, the child component prints a log, which means that the child useEffect dependency is completely invalidated because the reference keeps changing.
The subcomponent is concerned with values, not references, so one solution is to override the dependencies of the subcomponent and change props. Daphnisli to a string, as shown in the following code
const Child = (props= > {
useEffect(() = > {
console.log("daphnisli", props.daphnisli); },JSON.stringify(props.daphnisli)]);
return <div>Child</div>;
});
Copy the code
But the real culprit is the parent component. Another way to optimize the parent component is to use useRef:
function App() {
const [count, forceUpdate] = useState(0);
const daphnisli = useRef({ name: 'li3 shuang3' });
return (
<div>
<Child daphnisli={daphnisli.current} />
<div onClick={()= > forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
Copy the code
So daphnisli’s reference stays the same all the time.