1. The introduction
If you’re using React 16, try the Function Component style for more flexibility. But before you try it, you’d better read this article first to get a preliminary understanding of the mental mode of Function Component and avoid the trouble caused by out-of-sync mental mode.
2. The intensive reading
What is a Function Component?
Function Component is the React Component created as a Function:
function App() {
return (
<div>
<p>App</p>
</div>
);
}
Copy the code
That is, a Function that returns a JSX or createElement can be treated as a React Component. This form of Component is called a Function Component.
So have I learned Function Component?
Don’t worry, the story is just beginning.
Q: What are Hooks?
Hooks are tools that assist Function Components. For example, useState is a Hook that can be used to manage state:
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
The first item in the array is the value, and the second item is the assignment function. The first argument to the useState function is the default value. Callbacks are also supported. See the Hooks rule for more details.
The value is assigned before setTimeout is printed
Let’s combine useState with setTimeout again and see what we find.
Create a button that incremented the counter but delayed it for 3 seconds before printing:
function Counter() {
const [count, setCount] = useState(0);
const log = (a)= > {
setCount(count + 1);
setTimeout((a)= > {
console.log(count);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
If we click three times in three seconds, the value of count will eventually change to 3, and the resulting output will be. ?
0
1
2
Copy the code
Yeah, that sounds right, but it’s a little weird, you know?
How about using Class Component?
On the blackboard, back to our familiar Class Component model, again the above functions:
class Counter extends Component {
state = { count: 0 };
log = (a)= > {
this.setState({
count: this.state.count + 1
});
setTimeout((a)= > {
console.log(this.state.count);
}, 3000);
};
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.log}>Click me</button>
</div>); }}Copy the code
Well, it’s equivalent, right? Three quick clicks of the button in three seconds, this time the result:
3
3
3
Copy the code
Function Component = Function Component = Function Component = Function Component
This is the first hurdle you must cross to use Function Component properly. Make sure you fully understand the following:
First of all, Class Component is explained:
- First of all state is Immutable,
setState
A new state reference is always generated. - But Class Component passes
this.state
Mode read state,This results in an updated reference to state being made every time the code is executedSo the result of three quick clicks is3 3 3
.
So for the Function Component:
useState
The generated data is also Immutable. When a new value is Set through the second argument of the array, the original value forms a new reference for the next rendering.- But because the read of state did not pass
this.
In a way that makesEvery timesetTimeout
Both read the closure environment of the rendering at the time, and although the latest value changed with the latest rendering, the state remained the old value in the old rendering.
To make it easier to understand, let’s simulate what happens when a button is clicked three times in Function Component mode:
The first time I clicked on it, I rendered it 2 times. SetTimeout takes effect on the first render, and the state is:
function Counter() {
const [0, setCount] = useState(0);
const log = (a)= > {
setCount(0 + 1);
setTimeout((a)= > {
console.log(0);
}, 3000);
};
return. }Copy the code
SetTimeout takes effect on the second render. At this point, the state is:
function Counter() {
const [1, setCount] = useState(0);
const log = (a)= > {
setCount(1 + 1);
setTimeout((a)= > {
console.log(1);
}, 3000);
};
return. }Copy the code
SetTimeout takes effect on the third render, when the state is:
function Counter() {
const [2, setCount] = useState(0);
const log = (a)= > {
setCount(2 + 1);
setTimeout((a)= > {
console.log(2);
}, 3000);
};
return. }Copy the code
As you can see, each render is a separate closure, and in a separate three render, count is 0, 1, 2 for each render, so no matter how long the setTimeout is delayed, the print will always be 0, 1, 2.
With that in mind, we can move on.
How do I get the Function Component to print3 3 3
?
Does this mean that the Function Component cannot override the functions of the Class Component? Not at all. I hope that by the end of this article, you will not only have solved this problem, but also have a better understanding of why code implemented with Function Component is better and more elegant.
The first option is to take advantage of a new hook-Useref capability:
function Counter() {
const count = useRef(0);
const log = (a)= > {
count.current++;
setTimeout((a)= > {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
The printout for this scenario is 3, 3, 3.
To understand why, first understand what useRef does: An object created with useRef has only one value and is shared between all Rerenders.
So if we assign or read count.current, we always read its latest value, regardless of the rendering closure, so if we hit three quick clicks, we’re bound to return 3, 3, 3.
The problem with this approach, however, is that useRef is used instead of useState to create values, so the natural question is how do you achieve the same effect without changing the way the original values are written?
How to print without modifying the original values3 3 3
?
One of the easiest ways to do this is to create a new useRef value for setTimeout and use the original count for the rest of the program:
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect((a)= > {
currentCount.current = count;
});
const log = (a)= > {
setCount(count + 1);
setTimeout((a)= > {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
From this example, we introduce a new, and most important, hook-use effect, which you must understand in depth.
UseEffect handles side effects, which are executed after each Render. In other words, each Render is executed, but only after the actual DOM operation is completed.
We can use this feature to assign the latest value of count to currentCount. Current after each render, so that currentCount’s value is automatically synchronized with the latest value of count.
To make sure you understand useEffect correctly, I break down its execution cycle into each rendering. Suppose you hit the button three times quickly in three seconds, then you need to simulate in your mind what happens in the following three renderings:
On the first click, the useEffect takes effect on the second render:
function Counter() {
const [1, setCount] = useState(0);
const currentCount = useRef(0);
useEffect((a)= > {
currentCount.current = 1; // Execute once after the second render
});
const log = (a)= > {
setCount(1 + 1);
setTimeout((a)= > {
console.log(currentCount.current);
}, 3000);
};
return. }Copy the code
On the second click, the useEffect takes effect on the third render:
function Counter() {
const [2, setCount] = useState(0);
const currentCount = useRef(0);
useEffect((a)= > {
currentCount.current = 2; // Execute once after the third render
});
const log = (a)= > {
setCount(2 + 1);
setTimeout((a)= > {
console.log(currentCount.current);
}, 3000);
};
return. }Copy the code
On the third click, the useEffect takes effect on the fourth render:
function Counter() {
const [3, setCount] = useState(0);
const currentCount = useRef(0);
useEffect((a)= > {
currentCount.current = 3; // Execute once after the fourth render
});
const log = (a)= > {
setCount(3 + 1);
setTimeout((a)= > {
console.log(currentCount.current);
}, 3000);
};
return. }Copy the code
Notice how the contrast differs from the setTimeout rendering expanded in the section above.
Note that useEffect also varies from rendering to rendering, and that the useEffect closure environment is completely independent between renderings of the same component. For this example, useEffect is executed four times, with the following four assignments ending up at 3:
currentCount.current = 0; // Render the first time
currentCount.current = 1; // Second render
currentCount.current = 2; // Render the third time
currentCount.current = 3; // Rendering for the fourth time
Copy the code
Make sure you understand this sentence before reading on:
- In the example of setTimeout, three clicks trigger four renderings, but setTimeout takes effect on the first, second, and third render, respectively, so the values are 0, 1, 2.
- In the useEffect example, three clicks also trigger four renderings, but useEffect takes effect on the first, second, third, and fourth renderings respectively, eventually changing currentCount to 3.
Wrap with custom hooksuseRef
Get tired of writing a bunch of useEffect synchronization data to useRef every time? Yes, to simplify, we need to introduce a new concept: custom Hooks.
First, custom Hooks allow you to create custom Hooks, as long as the function name begins with use and returns a non-JSX element. All custom Hooks, including the built-in ones, can also be invoked within custom Hooks.
We can write useEffect to our custom Hook:
function useCurrentValue(value) {
const ref = useRef(0);
useEffect((a)= > {
ref.current = value;
}, [value]);
return ref;
}
Copy the code
Dependences is the second parameter of useEffect. This parameter defines a useEffect dependency. the useEffect is not executed in a new render as long as all references to the dependencies are unchanged. If the dependency is [], the useEffect is only initialized once. Subsequent Rerender will never be executed.
In this example, we tell React: synchronize the latest value to ref.current only when the value changes.
This custom Hook can then be called from any Function Component:
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useCurrentValue(count);
const log = (a)= > {
setCount(count + 1);
setTimeout((a)= > {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Copy the code
The code is much cleaner after encapsulation and, most importantly, the logic is encapsulated. We just need to understand that the Hook useCurrentValue can produce a value whose latest value is always synchronized with the incoming parameter.
Set useEffect second parameter to an empty array, and the custom Hook represents the didMount lifecycle!
Yes, but I recommend that you stop thinking about the life cycle as it prevents you from understanding Function Component better. Because the next topic, is to tell you: always be honest about the useEffect dependency, the parameter of the dependency must be filled in, otherwise it will be very difficult to detect and fix the BUG.
willsetTimeout
Switch tosetInterval
How will
Let’s go back to the starting point and replace the first setTimeout Demo with setInterval and see what happens:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= >clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code
This example will lead to the second obstacle to learning about The Function Component. Understanding it will lead to a deeper understanding of the rendering principle of the Function Component.
First, let’s introduce a new concept, the return value of the useEffect function. Its return value is a function that, when useEffect is about to be re-executed, executes the first callback of Rerender useEffect before the first callback of useEffect for the next rendering.
Taking two consecutive renderings as an example, this is what it looks like when unfolded:
First render:
function Counter() {
useEffect((a)= > {
// Execute after the first render
// The final execution order is 1
return (a)= > {
// Since the dependencies are not filled in, the second render useEffect is executed again, and the callback function for this place in the first render is called before execution
// The final execution order is 2}});return. }Copy the code
Second rendering:
function Counter() {
useEffect((a)= > {
// Execute after the second render
// The final execution order is 3
return (a)= > {
// and so on}});return. }Copy the code
However, this Demo sets useEffect’s second argument to [], so its return function will only be executed when the component is destroyed.
As you can see from the previous example, this Demo wants to take advantage of the [] dependency, use useEffect as didMount, and incrementally count every time with setInterval, so it expects to incrementally count by 1 per second.
The result:
1
1
1.Copy the code
Readers who understand the setTimeout example should be able to deduce the reason for this for themselves: setInterval is always in the first Render closure, and count is always 0, which is equivalent to:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(0 + 1);
}, 1000);
return (a)= >clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code
The main culprit, however, is not being honest about the dependency. In this example, useEffect relies on count, but the dependency must be written as [], so the error is difficult to understand.
So the way to correct it is to be honest with the dependency.
Always be honest with dependencies
Once we are honest about our dependence, we can get the right results:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(count + 1);
}, 1000);
return (a)= > clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
Copy the code
We use count as a dependency on useEffect and get the correct result:
1 2 3...Copy the code
Since the risk of missing dependencies is so great, there is also a safeguard, which is the eslint-plugin-React-hooks plugin that automatically corrects dependencies in your code. You can’t be honest about dependencies!
For this example, however, the code is still bugged: the counter is re-instantiated every time, and the performance cost would be unacceptable if it were for any other laborious operation.
How do I not reinstantiate each renderingsetInterval
?
The easiest way to do this is to take advantage of the second assignment use of useState, which does not directly depend on count, but instead assigns the value as a function callback:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(c= > c + 1);
}, 1000);
return (a)= >clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code
This is what it really does:
- Do not rely on
count
So be honest about dependence. - The dependency of
[]
, only initialization will be rightsetInterval
Instantiate.
And the reason why the output is still correct is 1, 2, 3… The reason is that in the setCount callback function, the c value always points to the latest count value, so there is no logical loophole.
But if you think about it more carefully, you’ll find a new problem: if there are more than two variables to use, it won’t work.
When you use more than one variable at a time?
If you want to add both count and step, then the useEffect dependency must be written to one of the values, and the frequent instantiation problem will arise again:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
useEffect((a)= > {
const id = setInterval((a)= > {
setCount(c= > c + step);
}, 1000);
return (a)= > clearInterval(id);
}, [step]);
return <h1>{count}</h1>;
}
Copy the code
In this example, since setCount only gets the latest count value, in order to get the latest step value each time, the step must be declared in the useEffect dependency, resulting in the setInterval being instantiated frequently.
Naturally, this problem also bothered the React team, so they came up with a new Hook to solve the problem: useReducer.
What is a useReducer
Don’t think about Redux for a moment. Just consider the above scenario and see why the React team listed useReducer as one of the built-in Hooks.
First introduce the usage of useReducer:
const [state, dispatch] = useReducer(reducer, initialState);
Copy the code
UseReducer returns the same structure as useState, except that the second item in the array is Dispatch and the first parameter is reducer.
The reducer defines how data is transformed. For example, a simple reducer looks like this:
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: state.count + 1
};
default:
returnstate; }}Copy the code
You can increment the count by calling Dispatch ({type: ‘increment’}).
So going back to this example, we just need to rewrite the usage a little bit:
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect((a)= > {
const id = setInterval((a)= > {
dispatch({ type: "tick" });
}, 1000);
return (a)= > 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
As can be seen, we have completed the accumulation of count by using the tick type of reducer, but in the useEffect function, count and step are completely bypassed. So useReducer is also known as the “dark magic” to solve such problems.
Whatever you call it, the essence of this is to decouple functions from data, so that functions simply issue instructions and do not need to re-initialize themselves when the data they use is updated.
Careful readers will notice that this example still has one dependency, the Dispatch, but the dispatch reference never changes, so you can ignore its effects. It also shows you have to be honest about your dependencies no matter what.
This brings up another caveat: try to write the function inside useEffect.
I’m going to write the function hereuseEffect
internal
To avoid missing dependencies, we must write the functions inside useEffect so that eslint-plugin-react-hooks can statically complete the dependencies:
function Counter() {
const [count, setCount] = useState(0);
useEffect((a)= > {
function getFetchUrl() {
return "https://v? query=" + count;
}
getFetchUrl();
}, [count]);
return <h1>{count}</h1>;
}
Copy the code
The function getFetchUrl relies on count, and if you define this function outside of useEffect, neither the machine nor the human eye can see that useEffect’s dependency contains count.
However, this raises a new question: wouldn’t it be difficult to maintain all functions written inside useEffect?
How do I draw the functionuseEffect
The outside?
To solve this problem, we will introduce a new Hook: useCallback, which solves the problem of drawing functions outside useEffect.
Let’s start with the use of useCallback:
function Counter() {
const [count, setCount] = useState(0);
const getFetchUrl = useCallback((a)= > {
return "https://v? query=" + count;
}, [count]);
useEffect((a)= > {
getFetchUrl();
}, [getFetchUrl]);
return <h1>{count}</h1>;
}
Copy the code
As you can see, useCallback also has a second parameter, a dependency, and we’re packaging the dependencies of the getFetchUrl function into a new getFetchUrl function via useCallback, So useEffect just has to rely on getFetchUrl, so it has an indirect dependence on count.
In other words, we’re using useCallback to pull the getFetchUrl function outside of useEffect.
whyuseCallback
比 componentDidUpdate
Better to use
Recall the Class Component pattern, how we recount function arguments when they change:
class Parent extends Component {
state = {
count: 0.step: 0
};
fetchData = (a)= > {
const url =
"https://v? query=" + this.state.count + "&step=" + this.state.step;
};
render() {
return <Child fetchData={this.fetchData} count={count} step={step} />; } } class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } componentDidUpdate(prevProps) { if ( this.props.count ! == prevProps.count && this.props.step ! == prevProps. Step // Don't miss it!) { this.props.fetchData(); } } render() { // ... }}Copy the code
The above code should be familiar to anyone who has used Class Component regularly, but the problems are not trivial.
We need to understand that props. Count props. Step is used by props. FetchData.
But the question is, is this understanding too expensive? If I didn’t write the parent function fetchData, how do I know it depends on props. Count and props. Step without reading the source code? More seriously, if the fetchData is dependent on params on a given day, the downstream functions will need to fully override the logic in componentDidUpdate, otherwise the fetchData will not be re-counted when Params changes. As you can imagine, this approach is costly to maintain, if not impossible.
Think Function Component instead! Try using the useCallback mentioned earlier:
function Parent() {
const [ count, setCount ] = useState(0);
const [ step, setStep ] = useState(0);
const fetchData = useCallback((a)= > {
const url = 'https://v/search? query=' + count + "&step=" + step;
}, [count, step])
return (
<Child fetchData={fetchData} />
)
}
function Child(props) {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
}
Copy the code
Can see that when fetchData depends on the change, press the save button, eslint – plugin – react – hooks up automatically updated, and downstream of the code does not need to do any change, need to be concerned with only depend upon the fetchData downstream this function, As for what this function depends on, it’s already packaged in the useCallback and passed through.
Not only does it solve the maintainability problem, but useEffect is especially good for reexecuting logic whenever parameters change. Thinking in this way makes your code more “intelligent”, while thinking in a split lifecycle leaves your code fragmented and vulnerable to missing all kinds of timing.
UseEffect is a convenient abstraction of the business. Here are a few examples:
- Dependencies are query parameters, so
useEffect
If the query parameter changes, the list will be refreshed automatically. Notice that we changed the timing from the trigger to the receiver. - When the list is updated, re-register the drag-and-drop response events. Again, the dependent parameter is the list, and as soon as the list changes, the drag response is reinitialized, so we can safely modify the list without worrying about the drag event invalidation.
- As soon as one of the data streams changes, the page title changes synchronously. Similarly, there is no need to change the title every time the data changes
useEffect
“Listening” for changes in data, that’s one“Inversion of Control”Thinking.
Having said all that, the essence is to use the useCallback to separate functions out of useEffect.
So thinking further, can functions be detached from the entire component?
This is also possible, with the flexibility to implement it using custom Hooks.
Draw functions outside the component
In the fetchData function above, if you want to draw outside the entire component, you do it not with useCallback, but with custom Hooks:
function useFetch(count, step) {
return useCallback((a)= > {
const url = "https://v/search? query=" + count + "&step=" + step;
}, [count, step]);
}
Copy the code
As you can see, we have packaged the useCallback into our custom Hook useFetch, so it only takes one line of code in the function to achieve the same effect:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const fetch = useFetch(count, step); // Encapsulate useFetch
useEffect((a)= > {
fetch();
}, [fetch]);
return (
<div>
<button onClick={()= > setCount(c => c + 1)}>setCount {count}</button>
<button onClick={()= > setStep(c => c + 1)}>setStep {step}</button>
<button onClick={()= > setOther(c => c + 1)}>setOther {other}</button>
</div>
);
}
Copy the code
As it becomes easier to use, we can focus on performance. It can be observed that count and step change frequently, and each change will lead to the change of useCallback dependency in useFetch, which will lead to the regenerating function. In practice, however, such functions do not need to be regenerated every time, and there is a significant performance cost to generating them repeatedly.
Here’s another example to make this clearer:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const drag = useDraggable(count, step); // Encapsulates the drag and drop function
}
Copy the code
Let’s say we use Sortablejs to drag and drop an area. The performance penalty for this function to be repeated each time is very high. However, this function may rely on a count step variable that is not actually used because it only reports some logs:
function useDraggable(count, step) {
return useCallback((a)= > {
// Report logs
report(count, step);
// The initialization of the region is very time-consuming
/ /... Omit time-consuming code
}, [count, step]);
}
Copy the code
In this case, the dependence of the function is particularly unreasonable. While a dependency change should trigger function re-execution, if the cost of re-execution of the function is very high and the dependency is just an afterthought, the loss is not worth the gain.
Use the Ref to ensure that the time dependent function remains unchanged
One way to do this is by turning dependencies into refs:
function useFetch(count, step) {
const countRef = useRef(count);
const stepRef = useRef(step);
useEffect((a)= > {
countRef.current = count;
stepRef.current = step;
});
return useCallback((a)= > {
const url =
"https://v/search? query=" + countRef.current + "&step=" + stepRef.current;
}, [countRef, stepRef]); // The dependency will not change, but it will get the latest value each time
}
Copy the code
This method is more clever, ** the area to be updated is separated from the time consuming area, ** the content to be updated is provided to the time consuming area through the Ref, to achieve performance optimization.
While this is expensive to change the function, there is a more general way to solve this problem.
Common custom Hooks solve function reinstantiation problems
We can use useRef to create a custom Hook instead of useCallback, so that when the value of the dependent changes, the callback will not be re-executed, but can get the latest value!
The magic Hook is written as follows:
function useEventCallback(fn, dependencies) {
const ref = useRef(null);
useEffect((a)= > {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback((a)= > {
const fn = ref.current;
return fn();
}, [ref]);
}
Copy the code
Once again, I realized the power of custom hooks.
Let’s start with this one:
useEffect((a)= > {
ref.current = fn;
}, [fn, ...dependencies]);
Copy the code
When the fn callback changes, ref.current repoints to the latest FN logical norm. The point is that when dependencies change, the value of ref.current is assigned again, so that the value inside FN is up to date, and the next section of code:
return useCallback((a)= > {
const fn = ref.current;
return fn();
}, [ref]);
Copy the code
It is executed only once (the ref reference does not change), so each time you return dependencies as the latest FN, and fn is not yet executed again.
If we call the callback function passed in to useEventCallback X, then the code means that every time the closure is rendered, the callback function X always gets the one in the latest Rerender closure, so the value of the dependency is always up to date and the function is not reinitialized.
React officially does not recommend using this paradigm, so for this scenario, useReducer is used and functions are called through Dispatch. Remember that? Dispatch is a dark magic that can bypass dependencies, as we mentioned in the “What is a useReducer” section.
As you use Function Component, you become concerned about Function performance, which is great. The next natural focus is on Render’s performance.
Made PureRender memo
In Fucntion Component, the equivalent of Class Component’s PureComponent is react. memo.
const Child = memo((props) = > {
useEffect((a)= > {
props.fetchData()
}, [props.fetchData])
return (
// ...)})Copy the code
A component wrapped in memo makes a light comparison of each props item when it rerenders itself. If the reference does not change, it will not trigger the rerendering. So Memo is a great performance tuning tool.
Here’s a rendering optimization function that may seem more difficult to use than Memo, but when you really understand it, it’s actually more useful than Memo: useMemo.
Do a local PureRender with useMemo
Memo = memo = memo = memo = memo = memo = memo = memo
const Child = (props) = > {
useEffect((a)= > {
props.fetchData()
}, [props.fetchData])
return useMemo((a)= > (
// ...
), [props.fetchData])
}
Copy the code
As you can see, we wrapped the rendering code in useMemo so that even if the function Child is re-executed because of the props change, it will not be re-rendered as long as the props. FetchData used by the rendering function is unchanged.
This is where the first benefit of useMemo is found: more fine-grained optimized rendering.
If the function Child uses props A and props B as A whole, but the rendering only uses B, then the change to A will result in re-rendering using the memo scheme, but not using useMemo.
But the benefits of useMemo don’t stop there, so let me give you a hint. Let’s start with a new problem: Using props to pass functions and values between components when there are more and more parameters:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const fetchData = useFetch(count, step);
return <Child fetchData={fetchData} setCount={setCount} setStep={setStep} />;
}
Copy the code
While Child can be optimized via memo or useMemo, ** when the program is complex, there may be cases where multiple functions are shared among all Function Components ** and a new Hook: useContext is needed.
Context is used for transparent batch transmission
In the Function Component, you can create a Context using React. CreateContext:
const Store = createContext(null);
Copy the code
Null is the initial value, so it’s ok to set it to null. The next two steps are to use store. Provider in the root node and Hook useContext in the child node to get the injected data:
Use store. Provider injection on the root node:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const fetchData = useFetch(count, step);
return (
<Store.Provider value={{ setCount.setStep.fetchData}} >
<Child />
</Store.Provider>
);
}
Copy the code
Use useContext in the child node to get the injected data (i.e., the value of store.provider) :
const Child = memo((props) = > {
const { setCount } = useContext(Store)
function onClick() {
setCount(count= > count + 1)}return (
// ...)})Copy the code
In this way, there is no need to pass parameters through each function, and the public functions can all be in the Context.
However, when there are too many functions, the value of the Provider will become very bloated. We can solve this problem with the useReducer mentioned before.
useuseReducer
Pass content thin for Context
With useReducer, all callbacks are done by calling Dispatch, so the Context simply passes dispatch a function:
const Store = createContext(null);
function Parent() {
const [state, dispatch] = useReducer(reducer, { count: 0.step: 0 });
return (
<Store.Provider value={dispatch}>
<Child />
</Store.Provider>
);
}
Copy the code
This makes both the root node Provider and child element calls much cleaner:
const Child = useMemo((props) = > {
const dispatch = useContext(Store)
function onClick() {
dispatch({
type: 'countInc'})}return (
// ...)})Copy the code
You might quickly think, wouldn’t it be better to inject state through a Provider as well? Yes, but be aware of potential performance issues here.
willstate
Put that in Context
A little bit of a twist is to put state in the Context as well, which makes assigning and valuing very convenient!
const Store = createContext(null);
function Parent() {
const [state, dispatch] = useReducer(reducer, { count: 0.step: 0 });
return (
<Store.Provider value={{ state.dispatch}} >
<Count />
<Step />
</Store.Provider>
);
}
Copy the code
We need to be careful with the two children of Count Step, if we implement them like this:
const Count = memo((a)= > {
const { state, dispatch } = useContext(Store);
return (
<button onClick={()= > dispatch("incCount")}>incCount {state.count}</button>
);
});
const Step = memo((a)= > {
const { state, dispatch } = useContext(Store);
return (
<button onClick={()= > dispatch("incStep")}>incStep {state.step}</button>
);
});
Copy the code
The result: Clicking either incCount or incStep triggers the Rerender of both components.
The problem is that the memo can only be blocked in the outermost layer, while data injection through useContext occurs inside the function, bypassing the memo.
When state is changed by triggering dispatch, all components that use state are internally forced to refresh, and the only way to optimize the render times is to take out the useMemo!
useMemo
Cooperate withuseContext
Components that use useContext, if they don’t use props themselves, can use useMemo entirely instead of memo:
const Count = (a)= > {
const { state, dispatch } = useContext(Store);
return useMemo(
(a)= > (
<button onClick={()= > dispatch("incCount")}>
incCount {state.count}
</button>
),
[state.count, dispatch]
);
};
const Step = (a)= > {
const { state, dispatch } = useContext(Store);
return useMemo(
(a)= > (
<button onClick={()= > dispatch("incStep")}>incStep {state.step}</button>
),
[state.step, dispatch]
);
};
Copy the code
For this example, clicking the corresponding button will rerender only the components in use as expected. When used with the eslint-plugin-React-hooks plugin, even the second parameter dependencies of useMemo are autocomplete.
After reading this, do you think of Redux’s Connect?
When we compare Connect to useMemo, the similarities are striking.
A common Redux component:
const mapStateToProps = state= > (count: state.count);
const mapDispatchToProps = dispatch= > dispatch;
@Connect(mapStateToProps, mapDispatchToProps)
class Count extends React.PureComponent {
render() {
return (
<button onClick={()= > this.props.dispatch("incCount")}>
incCount {this.props.count}
</button>); }}Copy the code
A normal Function Component:
const Count = (a)= > {
const { state, dispatch } = useContext(Store);
return useMemo(
(a)= > (
<button onClick={()= > dispatch("incCount")}>
incCount {state.count}
</button>
),
[state.count, dispatch]
);
};
Copy the code
The effect of these two pieces of code is exactly the same. In addition to being cleaner, the Function Component has an even greater advantage: fully automated dependency derivation.
One reason for the creation of Hooks is to simplify the cost of using Immutable streams for static analysis of dependencies.
Let’s look at the Connect scene:
We need to write mapStateToProps in advance because we don’t know what data is being used by the child component. When we need to use a new variable in the data stream, we can’t access it in the component. We need to go back to mapStateToProps and add this dependency and use it again in the component.
UseContext + useMemo
Since state is fully injected, it is possible to use whatever you want in the Render function directly. When you press the save key, eslint-plugin-React-hooks automatically add external variables used in the code via static analysis in the second argument of useMemo. Such as state.count and dispatch.
In addition, it can be found that Context is very like Redux, so how to use useReducer to do the asynchronous fetching of the asynchronous middleware implemented in Class Component mode? The answer is: no.
It’s not that the Function Component can’t do asynchronous fetching, it’s just that the tool is wrong.
Use custom hooks to handle side effects
For example, in the asynchronous fetch scenario thrown above, the best approach for Function Component is to encapsulate a custom Hook:
const useDataApi = (initialUrl, initialData) = > {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false.isError: false.data: initialData
});
useEffect((a)= > {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
const result = await axios(url);
if(! didCancel) { dispatch({type: "FETCH_SUCCESS".payload: result.data }); }}catch (error) {
if(! didCancel) { dispatch({type: "FETCH_FAILURE"}); }}}; fetchData();return (a)= > {
didCancel = true;
};
}, [url]);
const doFetch = url= > setUrl(url);
return { ...state, doFetch };
};
Copy the code
As you can see, the custom Hook has a full life cycle. We can encapsulate the fetching process to expose only the states – loading or not: isLoading or not fetching failure: isError: data.
It is very convenient to use in components:
function App() {
const { data, isLoading, isError } = useDataApi("https://v", {
showLog: true
});
}
Copy the code
If this value needs to be stored in the data stream and shared between all components, we can combine useEffect with useReducer:
function App(props) {
const { dispatch } = useContext(Store);
const { data, isLoading, isError } = useDataApi("https://v", {
showLog: true
});
useEffect((a)= > {
dispatch({
type: "updateLoading",
data,
isLoading,
isError
});
}, [dispatch, data, isLoading, isError]);
}
Copy the code
How to use DefaultProps for a Function Component?
How to handle DefaultProps for Function Component?
The question seems simple, but it is not. There are at least two ways to assign DefaultProps to a Function Component, which are described below.
For Class Component, there is basically only one way to write DefaultProps:
class Button extends React.PureComponent {
defaultProps = { type: "primary".onChange: (a)= >{}}; }Copy the code
In Function Component, however, things are different.
Use ES6 features to assign values during the parameter definition phase
function Button({ type = "primary", onChange = () = >{} {}})Copy the code
This may seem elegant, but there is one major pitfall: the props that don’t hit are different every time the render reference is made.
Look at this scene:
const Child = memo(({ type = { a: 1}}) = > {
useEffect((a)= > {
console.log("type", type);
}, [type]);
return <div>Child</div>;
});
Copy the code
As long as the reference to type does not change, useEffect will not be executed frequently. Now that flushing through the parent element causes the Child to follow, we can see that each render prints out the log, which means that the reference to type is different each time.
There’s a less elegant way to solve this:
const defaultType = { a: 1 };
const Child = ({ type = defaultType }) = > {
useEffect((a)= > {
console.log("type", type);
}, [type]);
return <div>Child</div>;
};
Copy the code
The parent element is constantly refreshed, and the log is printed only once, because the reference to type is the same.
If you don’t want to maintain variable references separately, you can use the React built-in DefaultProps method to do so.
Use the React built-in solution
The React built-in solution is a good way to deal with frequent reference changes:
const Child = ({ type }) = > {
useEffect((a)= > {
console.log("type", type);
}, [type]);
return <div>Child</div>;
};
Child.defaultProps = {
type: { a: 1}};Copy the code
In the example above, the parent element is constantly refreshed, and the log is printed only once.
Therefore, it is recommended to use the React built-in solution for Function Component parameter defaults because the pure Function solution is not good for keeping references unchanged.
Finally, add a classic example of a parent component “pitting” a child component.
Don’t mess with the child components
Let’s make a click-add button as the parent component, so the parent component will refresh every time we click:
function App() {
const [count, forceUpdate] = useState(0);
const schema = { b: 1 };
return (
<div>
<Child schema={schema} />
<div onClick={()= > forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
Copy the code
In addition, we pass schema = {b: 1} to the child component, which is a big hole.
The code for the child component is as follows:
const Child = memo(props= > {
useEffect((a)= > {
console.log("schema", props.schema);
}, [props.schema]);
return <div>Child</div>;
});
Copy the code
Whenever the parent props. Schema changes, the log is printed. The result, of course, was that every time the parent refreshed, the child component printed a log, meaning that the child component [props. Schema] was completely disabled because the reference kept changing.
Children care about values, not references, so one solution is to override the dependencies of children:
const Child = memo(props= > {
useEffect((a)= > {
console.log("schema", props.schema); },JSON.stringify(props.schema)]);
return <div>Child</div>;
});
Copy the code
This ensures that the child component is rendered only once.
But the real culprit is the parent component, and we need to optimize the parent component using the Ref:
function App() {
const [count, forceUpdate] = useState(0);
const schema = useRef({ b: 1 });
return (
<div>
<Child schema={schema.current} />
<div onClick={()= > forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
Copy the code
In this way, the reference to the schema can always remain the same. If you have read this article in its full form, it should be well understood that the schema in the first example is a new reference in each rendering snapshot, whereas in the Ref example, the Schema has a unique reference in each rendering snapshot.
3. Summary
So are you getting started with Function Component?
What are the other error-prone details of the Function Component development process?
The discussion address is: Intensive Reading getting Started with Function Component · Issue #157 · DT-FE/Weekly
If you’d like to participate in the discussion, pleaseClick here to, each week has a new theme, released on the weekend or Monday. Front end intensive reading – to help you filter the right content.
Pay attention to the front end of the intensive reading wechat public account
special Sponsors
- DevOps full process platform
Copyright Notice: Free Reprint – Non-Commercial – Non-Derivative – Keep Your Name (Creative Sharing 3.0 License)