React Hooks Are currently used in the project and in the history project. The advantages of React Hooks compared to the Class component can be summarised as follows: Simple, no complex lifecycle in React hooks, no complex this pointing in class components, no complex component reuse schemes like HOC, Render props, etc. This article summarizes the experience of the React hooks project practice.
- Render behavior in React hooks
- Performance tuning in React hooks
- State management and communication in React hooks
Originally published on my blog: github.com/fortheallli…
React hooks render behavior
1. How is the React hooks component rendered
The key to understanding React hooks is that each render of the hooks component is independent, and each render is a separate scope with its own props and States, event handlers, etc. To summarize:
Each render is an unrelated function with a completely separate function scope that executes the render function and returns the corresponding render result
A class component, on the other hand, has props and States that refer to the last render throughout its lifecycle.
The difference between the React hooks component and the class component in the rendering behavior is a bit confusing.
React hooks each render is a separate function that generates props and state for the render area. Next, look at the rendering behavior in the class component:
Class components generate props and state in the constructor of the class component at the beginning of rendering. All rendering is performed in a render function and no new props and state are generated in each render. Instead, it assigns values to the this.props and this.state that were originally initialized.
Remember React hooks render behavior in the project
Understanding the render behavior of React hooks tells us how to use them in our projects. First, because the React hooks component generates separate fields for use during each rendering, so child functions, variables, etc., inside the component are regenerated at each lifetime, we should reduce declaring functions inside the React hooks component.
Writing a:
function App() {
const [counter, setCounter] = useState(0);
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={()= > setCounter(prevState => ++prevState)}>
Increment
</button>
</div>
);
}
Copy the code
Method 2:
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={()= >onClick(setCounter)}>
Increment
</button>
</div>
);
}
Copy the code
The App component is a hook component. Remember that the React hooks function redeclares formatCounter every time we render, so it is not desirable. We recommend that functions be declared outside of the component if they have no dependencies on state and props within the component. If the function is strongly dependent on the state and props within the component, we’ll cover the methods of useCallback and useMemo in the next section.
Render1, Render2, render1, render2, render1, render2… So what do we do here? (the value of the referenced address can be changed)
Using useRef, we can create a “constant” that points to the same reference address throughout the rendering life of the component.
With useRef, you can do a lot of things, such as get the state from the previous render.
function App(){
const [count,setCount] = useState(0)
const prevCount = usePrevious(count);
return (
<div>
<h1>Now: {count}, before: {prevCount}</h1>
<button onClick={()= > setCount(count + 1)}>Increment</button>
</div>
);
}
function usePrevious(value) {
const ref = useRef();
useEffect((a)= > {
ref.current = value;
}, [value]);
return ref.current;
}
Copy the code
In the example above, the ref object created by useRef() is the same object throughout the usePrevious component cycle. We can update the ref. Current value to record the state of the previous render of the App component during the App component rendering process.
There is also a problem with usePrevious:
function usePrevious(value) {
const ref = useRef();
useEffect((a)= > {
ref.current = value;
}, [value]);
return ref.current;
}
Copy the code
The question is: why does the ref.current return when value changes refer to the value before value changed?
In other words:
Why is useEffect executed after return ref.current?
To explain this, let’s talk about the magical useEffect.
3. Magical useEffect
Render1,render2… render1,render2… Rendern, how do these render functions relate to each other? In usePrevious, why is useEffect executed after return ref.current? With these two questions in mind let’s look at the most amazing useEffect of the hooks component.
It can be summed up in one sentence:
Each render will generate a different render function, and each render will generate a different Effects via useEffect. Effects will sound after each render.
In addition to generating a different scope for each render, useEffect also generates a unique effects that will take effect once the render is completed, if the hooks component uses useEffect.
For example:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
In the example above, the completed logic is:
- Render initial content:
<p>You clicked 0 times</p>
- After rendering, this effect is called :{document.title = ‘You clicked 0 times’}.
- Click Click me
- Render new content Render content:
<p>You clicked 1 times</p>
- After rendering, this effect is called :() => {document.title = ‘You clicked 1 times’}.
In other words, effect is at the end of the queue of synchronized execution in each render and is executed after dom update or function return.
We are looking at an example of usePrevious:
function usePrevious(value) {
const ref = useRef();
useEffect((a)= > {
ref.current = value;
}, [value]);
return ref.current;
}
Copy the code
UsePrevios always returns the last value because of the useEffect mechanism, in a new rendering process, ref.current is returned before deps dependent update ref.current is performed.
Now we know that in a render, there are independent props, functions, functions, functions, effects, etc. In fact, in each render, almost everything is independent. Let’s finish with two examples:
(1)
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
setTimeout((a)= > {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
(2)
function Counter() {
const [count, setCount] = useState(0);
setTimeout((a)= > {
console.log(`You clicked ${count} times`);
}, 3000);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
In both cases, we Click the Click Me button five times in three, and the output is the same.
You clicked 0 times You clicked 1 times You clicked 2 times You clicked 3 times You clicked 4 times You clicked 5 times
To sum up, each render is almost independent and unique. Except for the objects created by useRef, other objects and functions have no dependencies.
Performance optimizations in React hooks
Declare functions independent of the functions of state and props outside of the hooks component to improve component performance and reduce the need to re-declare these functions every time you render. In addition, React Hooks provide useMemo and useCallback to optimize component performance.
(1).useCallback
In some cases we need to define functions or methods in the hooks component, so it is recommended to cache this method using useCallback, so that this function does not need to be redeclared during each rendering if the useCallback dependencies do not change
UseCallback takes two arguments. The first argument is the function to be cached, and the second argument is an array representing the dependency. If the dependency changes, a new function will be declared, otherwise the cached function will be returned.
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App(props) {
const [counter, setCounter] = useState(0);
const onClick = useCallback((a)= >{
setCounter(props.count)
},[props.count]);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={onClick}>
Increment
</button>
</div>
);
}
Copy the code
In the example above, we added the onClick method to the example in Chapter 1 and cached it so that we need to regenerate it only if the count in props changes.
(2).useMemo
UseMemo is similar to useCallback, except that instead of caching functions, useMemo caches objects (which can be JSX virtual DOM objects). Again, the cached object is returned if the dependency remains unchanged, otherwise a new object is created.
To optimize component performance, we recommend:
Any method or object declared in the React hooks component must be wrapped in useCallback or useMemo.
(3)useCallback, the comparison method of useMemo dependencies
Let’s see how useCallback, the useMemo dependency, compares before and after update
import is from 'shared/objectIs';
function areHookInputsEqual(nextDeps: Array
, prevDeps: Array
| null,
) {
if (prevDeps === null) {
return false;
}
if(nextDeps.length ! == prevDeps.length) {return false
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
Copy the code
Is method is defined as:
function is(x: any, y: any) {
return( (x === y && (x ! = =0 || 1 / x === 1/ y)) || (x ! == x && y ! == y) ); }export default (typeof Object.is === 'function' ? Object.is : is);
Copy the code
This is the compatibility of es6’s object. is method, that is, the dependency in useCallback and useMemo is compared by object. is before and after.
React hooks state management and communication
Local state management in React Hooks is much more brief than it is for class components, so how do we resolve communication between components if we use React hooks in our component?
(1) UseContext
The most basic idea is probably to use useContext to solve the problem of communication between components.
Such as:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = (a)= > setCount(count - 1)
let increment = (a)= > setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>)}function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>)}Copy the code
In this example, by createContext and useContext, you can use the context in the App’s subcomponent CounterDisplay to achieve some sense of component communication.
In addition, the industry has several simpler packages based on useContext for its integrity:
Github.com/jamiebuilds… Github.com/diegohaz/co…
But none of these essentially solve a problem:
If you have too many contexts, how do you maintain them
This means that in a scenario where a large number of components communicate, the code using context to communicate components is not readable. Context is not a new thing, although the use of useContext reduces the complexity of using context.
(2) Redux implements communication between components using hooks
Redux can also be used to communicate between hooks components. In other words:
Redux also makes sense in React hooks
There is a problem with hooks because there is no high-order component like connect in React-redux, Redux-react-hook or 7.1 hooks version of react-redux.
- redux-react-hook
Redux-react -hook provides StoreContext, useDispatch, and useMappedState to operate stores in Redux. For example, mapState and mapDispatch can be defined as follows:
import {StoreContext} from 'redux-react-hook';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root'),
);
import {useDispatch, useMappedState} from 'redux-react-hook';
export function DeleteButton({index}) {
// Declare your memoized mapState function
const mapState = useCallback(
state => ({
canDelete: state.todos[index].canDelete,
name: state.todos[index].name,
}),
[index],
);
// Get data from and subscribe to the store
const {canDelete, name} = useMappedState(mapState);
// Create actions
const dispatch = useDispatch();
const deleteTodo = useCallback(
() =>
dispatch({
type: 'delete todo',
index,
}),
[index],
);
return (
<button disabled={! canDelete} onClick={deleteTodo}>
Delete {name}
</button>
);
}
Copy the code
- React-redux 7.1 in hooks
React-redux hooks provide three main methods: useSelector(), useDispatch(), useStore() Corresponds to mapState, mapDispatch, and store instances directly in Redux.
A brief introduction to useSelector. UseSelector not only can get state from store, but also supports the function of deep comparison. If the corresponding state does not change before and after, it will not recalculate.
For example, the most basic usage:
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props= > {
const todo = useSelector(state= > state.todos[props.id])
return <div>{todo.text}</div>
}
Copy the code
Implement the cache function usage:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state= > state.todos,
todos => todos.filter(todo= > todo.isDone).length
)
export const DoneTodosCounter = (a)= > {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
export const App = (a)= > {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>)}Copy the code
In the above cache usage, as long as todos.filter(todo => todo.isdone).length does not change, recalculation is not done.