React Hook Practical Summary - How to Write Less Complex Projects with React Hook
I have been using React for two months, and I will summarize some of the gains about React Hook in the actual combat and the courses I bought in the past two months.
Why React Invented Hooks
The model of the React component is actually very intuitive, that is, the mapping from Model to View, where the Model corresponds to the state and props in React. As shown in the figure below:
In the past, we needed to deal with the details of how DOM nodes should change when the Model changes. Now, we only need to use JSX to describe the final presentation of the UI in a declarative way based on the data of the Model, because React will help you handle all the details of DOM changes. Moreover, when the state in the Model changes, the UI will automatically change, which is called data binding.
So, we can think of the presentation of the UI as the execution process of a function. Among them, the Model is the input parameter, and the execution result of the function is the DOM tree, which is the View. What React wants to ensure is that whenever the Model changes, the function will be re-executed and a new DOM tree will be generated, and then React will update the new DOM tree to the browser in the best way.
In this case, is it really appropriate to use Class as a component? When Class is used as the carrier of React components, does it use all its functions? If you think carefully, you will find that using Class is actually a bit far-fetched, mainly for two reasons.
On the one hand, React components do not inherit from each other. For example, you do not create a Button component and then create a DropdownButton to inherit Button. So, React does not actually take advantage of the inheritance feature of Classes.
On the other hand, because all UI is state-driven, methods of a class instance (i.e. component) are rarely called externally. Be aware that all methods of a component are called internally or automatically as lifecycle methods.
This is also why React provided the mechanism for function components very early. However, there was a limitation at that time that function components could not have internal state, must be pure functions, and could not provide a complete lifecycle mechanism. This greatly limited the large-scale use of function components.
The birth of the hook: find a way to hook any data source into any function component
In fact, if we continue to think along the lines of function components, we will find that if we want to make function components more useful, the goal is to add state to function components. This may not seem difficult.
Think about it briefly, function and object are different, and there is no instance of the object can save the state between multiple executions, it is bound to need a space outside the function to save the state, and to be able to detect its changes, which can trigger the function component re-rendering.
Thinking further, do we need such a mechanism to bind an external data to the execution of the function. When the data changes, the function can automatically re-execute. In this way, any external data that affects the UI presentation can be bound to the function component of React through this mechanism. In React, this mechanism is called Hooks.
So we can now understand why this mechanism is called Hooks. As the name suggests, Hook means “hook”. In React, Hooks hook a target result to a data source or event source that may change, so when the hooked data or event changes, the code that produces the target result will be re-executed to produce the updated result.
For the function component, this result is the final DOM tree; so the structure of Hooks can be shown in the following figure: The role of Hooks is to treat all data as a data source, hook into any function component, and trigger updates to the function component
Benefits of Hooks
Logical separation
Separation of concerns
Logical separation
Take the scenario of binding window size we mentioned just now as an example. If there are multiple components that need to readjust the layout when the user adjusts the browser window size, then we need to extract such logic into a common module for multiple components to use. With React thinking, in JSX we will render different components according to Size, for example:
1 | function render() { |
In the Class component scenario, we first need to define a Higher Order Component, which is responsible for listening for window size changes and passing the changed value as props to the next component.
1 | const withWindowSize = Component => { |
How to implement the same logic if using Hooks and function components. First we need to implement a Hooks:
1 | const getSize = () => { |
Separation of concerns
Another major benefit of Hooks: helping to separate concerns In addition to logic reuse, another major benefit that Hooks can bring is to help separate concerns, meaning that Hooks can make code for the same business logic as much as possible Aggregate together.
This is difficult to do in the past in the Class component. Because in the Class component, you have to scatter the code of the same business logic in different lifecycle methods of the class component. Therefore, by using Hooks to clearly isolate the business logic, the code can be easier to understand and maintain.
Reflection
Logic separation allows logic to be separated from the function component, reducing the length of the function component.
Separation of concerns allows related logic to be grouped together, such as in the same custom hook, rather than scattered throughout the function component.
Suddenly found that before like all kinds of useState, useEffect are stuffed into the function component is not very good, which led to the function component will be very long, and let all the logic in a function mixed, completely did not use the logic separation and the benefits of separation of concerns.
The role of several built-in hooks and their use
useState: Give function components the ability to maintain state
The useState Hook is used to manage the state, which allows the function component to have the ability to maintain state. That is, the state is shared between multiple renders of a function component. The following example shows the use of useState:
1 | import React, { useState } from 'react'; |
The useState Hook is summed up like this:
1.The parameter initialState of useState (initialState) is the initial value of the created state, which can be of any type, such as numbers, objects, arrays, etc. (if you want to cache a function, you should use useCallback. If the initialState of useState is a function, it will cache the return result of the function).
2.The return value of useState () is an array with two elements. The first array element is used to read the value of state, and the second is used to set the value of this state. It should be noted here that the variable of state (count in the example) is read-only, so we must set its value through the second array element setCount.
3. If we want to create multiple states, then we need to call useState multiple times.
Generally speaking, one principle we have to follow is: state
Value passed from props. Sometimes the value passed from props cannot be used directly, but must be displayed on the UI after a certain calculation, such as sorting. So what we need to do is to reorder every time we use it, or use some cache mechanism instead of putting the result directly into state.
The value read from the URL. For example, sometimes it is necessary to read the parameters in the URL as part of the state of the component. Then we can read it from the URL every time we need it, instead of reading it out and putting it directly into state.
The value read from cookie, localStorage. Generally speaking, it is also read directly every time it is used, rather than read it and put it in state.
useEffect: Execution side effect
A useEffect, as its name suggests, is used to perform a side effect. What are side effects?
Generally speaking, a side effect refers to a piece of code that has nothing to do with the current execution result. For example, to modify a variable outside of a function, to initiate a request, etc.
That is to say, during the execution of the function component, the execution of the code in the useEffect does not affect the rendered UI.
Corresponding to the Class component, the useEffect covers the three lifecycle methods of ComponentDidMount, participentDidUpdate and participentWillUnmount. However, if you are used to using Class components, do not follow the method of mapping the useEffect to one or several lifecycles. You just need to remember that the useEffect is determined and executed after each component is rendered.
There are also two special uses of useEffect: no dependencies, and dependencies as empty arrays. Let’s analyze it in detail.
- If there are no dependencies, it will be re-executed after each render.
- An empty array as a dependency is only triggered on the first execution.
In addition to these mechanisms, useEffect also allows you to return a function to do some cleaning operations when the component is destroyed. For example, removing event listeners.
So when defining dependencies, we need to pay attention to the following three points:
The variables defined in the dependency must be used in the callback function, otherwise declaring the dependency is actually meaningless.
- Dependencies are usually an array of constants, not variables. Because generally when creating callbacks, you know very well which dependencies are used.
- React will use shallow comparisons to compare whether dependencies have changed, so pay special attention to arrays or object types. If you create a new object every time, even if it is equivalent to the previous value, it will be considered that the dependency has changed. This is a place that can easily lead to bugs when starting to use Hooks
useCallback: Cache callback function
In React function components, every UI change is done by re-executing the entire function, which is very different from traditional Class components: there is no direct way in function components to maintain a state between multiple renderings.
For example, in the following code, we define an event handling function on the plus button to increment the counter by 1. However, because the definition is inside the function component, it is impossible to reuse the handleIncrement function between multiple renders, and a new one needs to be created each time:
1 | function Counter() { |
Each time the state of the component changes, the function component will actually be executed again. Each time it is executed, a new event handler function handleIncrement is actually created. This event handler function contains a closure of the count variable to ensure the correct result every time.
This also means that even if the count does not change, when the function component is re-rendered due to other state changes, this writing will create a new function each time. Creating a new event handler function, although it does not affect the correctness of the result, is actually unnecessary. Because doing so not only increases the overhead of the system, but more importantly: the way a new function is created each time will make the component that receives the event handler function need to be re-rendered.
useMemo: Cache the result of the calculation
The API signature of useMemo is as follows:
1 | useMemo(fn, deps); |
Here, fn is a calculation function that generates the required data. Generally speaking, fn will use some variables declared in deps to generate a result, which is used to render the final UI. This scenario should be easy to understand: if some data is calculated from other data, then only when the data used, that is, the dependent data, changes should be recalculated.
For example, for a list that displays user information, now the user name needs to be searched, and the UI needs to display the filtered user according to the search keyword, then such a function needs to have two states:
- User Tabular Data itself: from a request.
Search keywords: data entered by the user in the search box.
In this example, no matter why the component needs to be re-rendered, it actually needs to perform a filtering operation. But in fact, you only need to recalculate the data to be displayed when one of the two states of users or searchKey changes. So, at this time, we can use the useMemo Hook to implement this logic and cache the results of the calculation:
1 | //... |
At this time, if we combine the two Hooks of useMemo and useCallback, we will find an interesting feature, that is, the function of useCallback can actually be achieved with useMemo. For example, the following code uses useMemo to achieve the function of useCallback:
1 | const myEventHandler = useMemo(() => { |
useRef: Share data between multiple renders
In a class component, we can define class member variables so that we can store some data on the object through member properties. But in the function component, there is no such space to store data. Therefore, React uses a Hook like useRef to provide such functionality. The API signature of useRef is as follows:
1 | const myRefContainer = useRef(initialValue); |
Suppose you want to make a timer component, which has both start and pause functions. Obviously, you need to use window.setInterval to provide timing functions; and in order to be able to pause, you need to save the reference to the counter returned by window.setInterval somewhere, making sure that when you click the pause button, you can also use window.clearInterval to stop the timer. Then, the most suitable place to save the counter reference is useRef, because it can store data across renderings. The code is as follows:
1 | import React, { useState, useCallback, useRef } from "react"; |
The data saved using useRef is generally independent of the rendering of the UI, so when the value of ref changes, it will not trigger the re-rendering of the component, which is where useRef differs from useState.
In addition to storing data across rendering, useRef has an important function of saving references to a DOM node. We know that in React, there is almost no need to care about how the real DOM node is rendered and modified. But in some scenarios, we have to get references to real DOM nodes, so combine React’s ref properties
useContext: Define global state
React provides a Context mechanism that allows all components to create a Context in the component tree at the beginning of a component. In this way, all components in the component tree can access and modify the Context. Then in the function component, we can use a Hook like useContext to manage the Context.
The API signature of useContext is as follows:
1 | const value = useContext(MyContext); |
As mentioned earlier, a Context is available from a component tree where a component is the root component, so we need an API to be able to create a Context, which is the React.createContext API, as follows:
1 | const MyContext = React.createContext(initialValue); |
The MyContext here has a Provider property, which is generally used as the root component of the component tree. Here I still use the example of the official React doc to explain, namely: a theme switching mechanism. The code is as follows:
1 | const themes = { |
See here you may be a little curious, Context looks like a global data, why design such a complex mechanism, instead of directly using a global variable to save data?
The answer is actually very simple, that is, to be able to bind data. When the data of this Context changes, the components that use this data can be automatically refreshed. But if there is no Context, but a simple global variable, it is difficult to achieve.
But what we just saw is actually a static example of using Context, directly using thems.dark as the value of Context. So how to make it dynamic? For example, the common switch dark or light mode button is used to switch the theme of the entire page. In fact, dynamic Context does not require us to learn any new APIs, but uses the mechanism of React itself, which can be achieved with this line of code:
1 | <ThemeContext.Provider value={themes.dark}> |
As you can see, themes.dark is passed as a property value to the Provider component. If you want to make it dynamic, you just need to save it with a state. By modifying the state, you can dynamically switch the value of the Context. And in doing so, all places that use this Context will be automatically refreshed. For example, this code
1 | // ... |
Comparison of Hooks and Life Cycles
The lifecycle approach is to think of component creation and updates as a pipeline of what you can do on this pipeline. The Hooks approach is what to do when the state changes.
How to structure a React project
The Root of Software Complexity: Complex Dependencies
We often say that a project looks very complex. So how should we define this “complex”? If you think carefully, you will find that when a function requires layers of nested module dependencies, even if you feel that the idea is very smooth during development, but when you look back, or if you want others to understand a function implementation, you have to go through a very deep call chain. This is the direct reason why you feel complex. So we can say that the root of software complexity comes entirely from complex dependencies.
Under the components, actions, Hooks and other folders, they are classified according to functions. And this classification is often further divided according to technical functions, such as table, modals, pages, etc. This approach actually increases the complexity of the project structure and is inconvenient to develop, mainly in two aspects.
On the one hand, for a function, we cannot intuitively know which folders its related code is scattered in. For example, the classification function in CMS may have lists, drop-down boxes, dialog boxes, asynchronous request logic, etc., which are all in different folders.
On the other hand, when developing a function, switching source code can be very inconvenient. For example, when you write a category list function, you need to frequently switch back and forth between components, style files, actions, reducers, etc. Moreover, if the project is large, then you need to expand a very long tree structure to find the corresponding files, or use file search to navigate. However, the premise of file search navigation is that you also need to have a good understanding of the logic of the entire function and know which files are available.
The essence of this development difficulty is that the source code is not organized according to business functions, but split from a technical point of view. Therefore, for the organization of folders, we must organize the source code by domain. A domain-related folder is similar to the first scenario just mentioned, and it contains all the technical modules it needs, so it will be very convenient to understand the code implementation or switch navigation during development.
As you can see, the entire application contains at least modules such as article management, comments, classification, and users. First of all, we know that a React application must be composed of some technical components, such as components, routing, actions, store, etc. In the figure, I distinguish these components with different colors. However, if we scatter these technical components into different domain folders, each domain folder has its own compoents, routing, actions, store, etc. In this way, each folder is equivalent to a small project, containing all the source code related to itself, which is easy to understand and develop.