preface
This week’s bricks are almost finished, and it’s time for happy summary. These two weeks have been in the “pit” of hook function, unable to extricate themselves for a long time. Should not be called “pit”, or their own vegetables, not carefully read the document, a lot of thought is not important, but in the actual application is very key points are always ignored by their own. Therefore, I plan to spend more time to review and sort out some hook functions on the official website (in the process of sorting out, I think it is easier to find problems and sum up experience).
React has three basic hooks:
- useState
- useEffect
- useContext
React Hook: “Extra Hooks”
useState
UseState is relatively simple compared to the other hooks, which are mainly used to define variables. The official documentation is also very clear, which can be skipped by those who are already familiar with it
Meet — first acquaintance
const [count, setCount] = useState(0)
Copy the code
-
Call useState
What does the method do?Define a “state variable”.
-
useState
What parameters are required?The useState method takes a parameter as the value of the variable initialization. (In the example, the useState method is called to declare a “state variable” count, which defaults to 0.)
-
useState
What is the return value of the method?Returns the current state and the function that updates the state.
Know each other — use useState
React ensures that the setState function identifier is stable and does not change when the component is re-rendered. This is why it is safe to omit setState from useEffect or useCallback’s dependency list.
The useState method defines three variables, count, studentInfo, and subjectList, and then changes their values.
const [count, setCount] = useState(0)
const [studentInfo, setStudentInfo] = useState({name: 'xiaowen', age: 18, gender: 'woman'})
const [subjectList, setSubjectList] = useState([
{ id: 0, project_name: 'Chinese' },
{ id: 1, project_name: 'mathematics'}])Copy the code
- Change the count value to 1
setCount(1)
Copy the code
- Change the age attribute of the studentInfo object to 20. Add the weight attribute with a value of 90
setStudentInfo({ ... studentInfo, age: 20, weight: 90 })Copy the code
- Change the project_name property of the second subjectList array to sport; And add a third term
{id: 2, project_name: 'music'}
# Ignore deep copy here, there are many elegant ways: immutable. Js, immer.js, loadsh
let temp_subjectList = JSON.parse(JSON.stringify(subjectList))
temp_subjectList[1].project_name = 'sports'
temp_subjectList[2] = { id: 2, project_name: 'music' }
setSubjectList(temp_subjectList)
Copy the code
In actual development, we will use the Eslint plugin provided by React to check Hook rules and effect dependencies. When a dependency is detected, a warning will be given. If the warning is the setState function is missing, we can ignore it. (This will be added later when we talk about useEffect)
React ensures that the setState function identifier is stable and does not change when the component is re-rendered. This is why it is safe to omit setState from useEffect or useCallback’s dependency list. (website)
Functional update
If the new state needs to be computed using the previous state, you can pass the function to setState. This function takes the previous state and returns an updated value. The same applies to data of reference type, such as arrays. You can’t operate on variables directly.)
Use the variables defined above:
- Click the button to add count
<button onClick={() => setPrevCount (prevCount => ++prevCount)}>Copy the code
- Example Change the age attribute of the studentInfo object to 20
setStudentInfo(prevState => {
You can also use object.assign
return{... prevState, age: 20} })Copy the code
Lazy initial state
If the initial state needs to be computed through complex calculations, we can pass in a function that computes and returns the initial state, which is only called during the initial render:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
Copy the code
The practical application
In practical applications, we often encounter some complex data structures. If useState is used to define these complex data structures in every place, it will probably be tiring.
This is to share a plugin I used in my project, use-immer, some basic type data and “directly substituted data” (for example, if there is an array arR, modifying arR is directly assigned to setArr([…]). ), I will use useState to declare such data, and use the useImmer method provided by use-immer to declare most data of reference type. Let’s take a look at how it works
- The installation
npm install immer use-immer
Copy the code
- reference
import { useImmer } from 'use-immer'
Copy the code
- Redeclare what was used above
subjectList
const [subjectList, setSubjectList] = useImmer([
{ id: 0, project_name: 'Chinese' },
{ id: 1, project_name: 'mathematics'}])Copy the code
- Change the project_name property of the second subjectList array to sport; And add a third term
{id: 2, project_name: 'music'}
setSubjectList(draft => {
draft[1].project_name = 'sports'
draft[2] = { id: 2, project_name: 'music'}})Copy the code
Note that the setSubjectList method accepts a function that accepts a draft parameter, which can be understood as a copy of the variable subjectList. It is a way to write immutable, immer, use-immer, and immutable.
useEffect
As for useEffect, my personal advice is to read the introduction on the website first and then read the complete guide to useEffect. After watching it, we have a much deeper understanding of it. Here are just the notes I compiled during the learning process, and the content is the complete guide to useEffect simplified.
UseEffect is delayed after the browser draws, but is guaranteed to be executed before any new renders. React will refresh the effect of the last render before the component is updated.
Every render
Focus: Each rendering, the component has its own rendering:
- Props and State
- Event handler
- Effects
Props and State
Write a Counter component Counter:
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
Copy the code
When the Counter component first renders, it gets count from useState() with an initial value of 0. When we call setCount(1) React renders the component again with count of 1, and so on, each rendering is independent.
# During first render
function Counter() {
const count = 0; # Returned by useState()
#...
<p>You clicked {count} times</p>
#...
}
# After a click, our function is called again
function Counter() {
const count = 1; # Returned by useState()
#...
<p>You clicked {count} times</p>
#...
}
# After another click, our function is called again
function Counter() {
const count = 2; # Returned by useState()
#...
<p>You clicked {count} times</p>
#...
}
Copy the code
The count in the Counter component is simply a constant provided by React. When setCount is called, React calls the component again with a different count value. React then updates the DOM to keep it consistent with the rendered output.
The key is this: the count constant in any render does not change over time. The render output changes because the Counter component is called, and it contains count constants that are independent of each render resulting from the call. This means that each rendering of a component is independent of its props and state.
Event handler
Modify the Counter component Counter example.
Component content: There are two buttons, one to change the value of count and the other to display the popup after the 3s delay.
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
)
}
Copy the code
- Click the button to modify
count
Is 3. - Click another button to open the popover.
- Before the popover pops up, click the button to modify it
count
Is 5.
At this point, the popover displays a count value of 3.
Analysis:
The whole process was first rendered six times.
- Initialize render: render0;
- Modify the
count
Render1 -> render2 -> render3; - Click the button to open a popup with the component in “Render3 state”;
- Modify the
count
Render3 -> render5 -> render5;
Render0 -> render1 -> Render2 -> render3
function Counter() {
const count = 3
#...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
#...
}
Component state: Render3
# Trigger the event handler handleAlertClick, which captures count as 3 and opens a popover 3 seconds later.
function Counter() {
const count = 3
#...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: '+ 3)}, 3000)}#...
}
Render3 -> Render5 -> Render5
function Counter() {
const count = 5
#...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
#...
}
Copy the code
Effects
To modify the Counter component, click the button 3 times:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => {
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
Analysis: The entire process component was rendered four times:
- Initialization, render0: prints
You clicked 0 times
; - Modify the
count
The value is 1. Render1: printsYou clicked 1 times
; - Modify the
count
Value is 2. Render2: printsYou clicked 2 times
; - Modify the
count
Value is 3, render3: printsYou clicked 3 times
;
As you can see from the whole example, useEffect is also independent on each render.
It’s not that the value of coun t changes in the “immutable” effect, but that the effect function itself is different from one render to another.
Clear effect
When timers are used in useEffect or subscriptions are added, useEffect returns a function that clears timers or unsubscribes. But we need to know that clearance is lagging. (Here is a personal understanding, may not be accurate description)
For example, print the number of clicks in useEffect:
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`You clicked ${count} times`)
return() => {
console.log('destroy')}})return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
Copy the code
Click the button 3 times, the result printed in the console is as follows:
You clicked 0 times
- The destruction
You clicked 1 times
- The destruction
You clicked 2 times
- The destruction
You clicked 3 times
It is easy to see from the print that the previous effect was removed during the re-rendering.
Ps: What about the whole re-rendering process for components?
Let’s say render0 and Render1 are rendered twice:
- React Renders render1 UI;
- The browser draws and renders render1’s UI;
- React Removes render0 effect;
- React runs render1’s effect;
React will only run Effects after the browser has drawn. This makes your application much smoother because most effects don’t block updates on the screen.
Here is an example to prove this conclusion
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(99)
console.log(count)
return() => {
console.log('destroy')
}
})
console.log('I'll do it first! ')
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
Copy the code
Running the code, on the console we can see the following output (with the component state initialized: render0) :
- I will execute first!
- 0
- I will execute first!
- The destruction
- 99
- I will execute first!
According to the printed results, we can analyze:
- React render initialized UI (render0);
- perform
useEffect
; - call
setCount
Methods to modifycount
Value 99, component rerender (Render1); - Proceed with render0 in the order of execution
useEffect
To printcount
Is 0. (render0count
The value of 0) - React Re-render the UI (Render1);
- The implementation of render0
useEffect
Clear function of; - The implementation of render1
useEffect
; - call
setCount
Methods to modifycount
The value is 99 (the component is not re-rendered because the value passed in has not changed); - print
count
Is 99;
I will execute it first! When the component is called, React determines whether it needs to render. Then there are a series of steps above, please help to point out if there is any misunderstanding.
Set the dependence
In practice, we don’t need to execute effects every time a component is updated. Instead, we can set a dependency on useEffect to tell React when to execute useEffect.
In the following example, useEffect is executed only if the name changes. If the dependency is set to an empty array, the useEffect is executed only once.
useEffect(() => {
document.title = 'Hello, ' + name
}, [name])
Copy the code
Set up dependencies correctly
The first requirement is simple: add count by 1 every second through a timer.
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [])
Copy the code
We only want to set the setInterval timer once, so we set the dependency to [], but since the component has independent state and effects for each rendering, the count value in the above code is always 0. When setCount is executed once, Subsequent setCount operations are invalid.
In this case, we can add count to the dependency and solve the problem. The idea is correct, but this defeats our intent of “we only want setInterval to run once,” and may cause unnecessary bugs.
Solution: Use functional updates (as mentioned earlier).
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(preCount => preCount + 1)
}, 1000)
return () => clearInterval(id)
}, [])
Copy the code
But in practical application, this way is far from satisfying our needs. For example, when relying on multiple data:
function Counter() {
const [count, setCount] = useState(0)
const [step, setStep] = useState(1)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step)
}, 1000);
return () => clearInterval(id)
}, [step])
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
)
}
Copy the code
When we modify the step variable, the timer is reset. This is something we don’t want to see, so how do we optimize? At this point we need to use the useReducer.
When we want to update a state that depends on the value of another state, we may need to use useReducer to replace them.
import React, { useReducer, useEffect } from 'react'
import ReactDOM from 'react-dom'
const initialState = {
count: 0,
step: 1,
}
function reducer(state, action) {
const { count, step } = state
if (action.type === 'tick') {
return { count: count + step, step }
} else if (action.type === 'step') {
return { count, step: action.step }
} else {
throw new Error()
}
}
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>
<input value={step} onChange={e => {
dispatch({
type: 'step',
step: Number(e.target.value)
})
}} />
</>
)
}
Copy the code
React ensures that Dispatches remain constant for the duration of the component’s declaration cycle.
About the function
In useEffect, we may miss dependencies when we call functions defined externally. So we can put the function definition in useEffect.
But what happens when we have reusable functions defined externally?
- If the function does not use any values within the component, we can define it outside the component.
function getFetchUrl(query) {
return 'xxx? query=' + query
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react')
}, [])
useEffect(() => {
const url = getFetchUrl('redux')}, [])}Copy the code
- use
useCallback
The packing.
function SearchResults() {
const [query, setQuery] = useState('react')
const getFetchUrl = useCallback(() => {
return 'xxx? query=' + query
}, [query])
useEffect(() => {
const url = getFetchUrl()
}, [getFetchUrl])
}
Copy the code
If query remains unchanged, so does getFetchUrl, and our effect will not be rerun. But if query changes, getFetchUrl changes with it, so it rerequests the data.
UseCallback essentially adds a layer of dependency checking. It solves the problem in a different way – we make the function itself change only when needed, rather than removing the dependency on the function.
More on useReducer and useCallback in a later note.
useContext
Usage scenarios of useContext.
In a typical React application, data is passed top-down (from parent to child) through the props properties, but this is extremely cumbersome for certain types of properties (e.g. locale preferences, UI themes) that are required by many components in the application. Context provides a way to share such values between components without explicitly passing props layer by layer through the component tree. (Quoted from official website)
Let’s look at an example
- Creating the top-level component
Container
.
import React, { useState, createContext } from 'react'
import Child1 from './Child1'
import Child2 from './Child2'// Create a Context objectexport const ContainerContext = createContext({})
function Container() {
const [state, setState] = useState({child1Color: 'pink', child2Color: 'skyblue'})
const changeChild1Color = () => {
setState({ ... state, child1Color:'lightgreen'})}return( <> <ContainerContext.Provider value={state}> <Child1></Child1> <Child2></Child2> </ContainerContext.Provider> <button OnClick ={changeChild1Color}> Modify child1 color </button> </>)}export default Container
Copy the code
- Creating child components
Child1
.
import React, {useContext} from 'react'
import { ContainerContext } from './Container'
function Child1() {
const value = useContext(ContainerContext)
return<h1 style={{color: value.child1color}}> I am Child1 component </h1>}export default Child1
Copy the code
- Creating child components
Child2
.
import React, {useContext} from 'react'
import { ContainerContext } from './Container'
function Child2() {
const value = useContext(ContainerContext)
return<h1 style={{color: value.child2color}}> I am Child2 component </h1>}export default Child2
Copy the code
We can use this simple demo to understand the basics of useContext.
Analysis of basic use methods:
-
Create a Context object using the createContext method provided by React.
In the Container component, export const ContainerContext = createContext({}) creates a Context object and sets its default value to {}.
Note: Default values only take effect if there is no Provider match in the component’s tree.
Modify the components returned in Container slightly so that the context read by Child1 and Child2 is the default when the context object is created.
<> <Child1></Child1> </Child2> </Child2> </button onClick={changeChild1Color}>Copy the code
-
Each Context object returns a Provider React component, which allows the consuming component to subscribe to changes to the Context.
Container components in the Context object returns ContainerContext. Provider component, it receives a value attribute, is passed to the consumer component. Consumer components wrapped inside the context (Child1 and Child2) can subscribe to changes to the context.
A Provider React component can interact with multiple consumer components. Multiple Provider React components can also be nested. The inner layer overwrites the data in the outer layer.
-
In the consuming component, subscribe to the context using useContext.
Note: The argument to useContext must be the context object itself.
That’s the basic way to use it, but since I don’t use useContext very much, I won’t introduce it too much here.
It is worth noting:
- Components that subscribe to the context will always be rerendered when the context value changes. If rerendering a component is expensive, you can optimize it by using Memoization.
- Using the Context makes the component less reusable to some extent, and we need to make a reasonable trade-off.
conclusion
In the process of learning, I often have an illusion: I can. In fact, this kind of “will”, but also a certain knowledge point “familiar”. When it comes time to really start something, you’ll find you don’t know what to do. It’s like when you first started working on the front end, you’d see someone else’s code and you’d see it, but you couldn’t write it yourself. In addition, the two days that a lot of knowledge points have learned can be remembered, but a period of time will not be forgotten, and to learn again.
Therefore, I want to change this dilemma by sorting out my own learning process, so as to deepen my impression and facilitate future reference.
I hope this article is also helpful to you. If you have any suggestions, please leave a comment
Long winded so much, small partners give me a thumbs up, thank you ~
Reference article:
UseEffect complete guide to React