This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Yusen Meng
If you have used class-based components, you are definitely familiar with component lifecycle functions such as componentDidMount, componentDidUpdate, etc. If not, don't worry, in this article, you will see the implementation of components based on Hooks, which will be a very different way of thinking. You don't need to worry about what the lifecycle of a component is at all.
React provides very few Hooks, there are only 10 in total, such as useState, useEffect, useCallback, useMemo, useRef, useContext, etc. We will first talk about the two most core Hooks, useState and useEffect.
useState: Giving Function Components the Ability to Maintain State.
The useState Hook is used to manage state in React components, providing function components with the ability to maintain state and share it across multiple renderings. The following example demonstrates the usage of useState.
import React, { useState } from 'react';
function Example() {
// Create state to store the count value and initialize it to 0
const [count, setCount] = useState(0);
return (
<div>
{/* Display the count value */}
<p>{count}</p>
{/* On clicking the button, call the setCount function and update the count value by adding 1 */}
<button onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}
To summarize the usage of the useState Hook:
- The
initialStateparameter inuseState(initialState)is the initial value of the state and can be of any type, such as numbers, objects, or arrays. -
useState()returns an array with two elements. The first element is used to access the value of the state, and the second is used to set its value. Note that the state variable (in this case "count") is read-only and must be set through the second element,setCount. - If you need multiple states, simply call
useStatemultiple times. For example, to create multiple states, the code would look like this:
// Declare a state for age with an initial value of 42
const [age, setAge] = useState(42);
// Declare a state for fruit with an initial value of "banana"
const [fruit, setFruit] = useState('banana');
// Declare a state for an array with an initial value of an array with one todo item
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
useState is a simple Hook that makes it easy to create states and provides a specific method, such as setAge, to set the state. It is similar to setState in class components, but with the advantage of being able to create multiple states in functional components. Unlike class components, which can only have one state and typically represent different states through different properties of an object, useState in functional components provides a more semantic way of creating multiple states.
The state is a critical mechanism in React components, but it's important to consider what values should be stored in it. In general, it's best to follow the principle that state should not store values that can be calculated.
For example, values passed from props, such as a result from a calculation, should not be stored in the state. It is more efficient to perform the calculation every time it's needed rather than storing the result in the state.
Similarly, values read from the URL, cookies, or localStorage should be read directly when needed rather than being stored in the state.
useEffect: Side Effects.
A side effect is a code that is unrelated to the current function's result. It includes actions such as modifying a variable outside the function or making a request. The execution of code in useEffect does not affect the current function component's rendered UI.
useEffect(callback, dependencies)
The first argument is a callback function that will be executed, and the second argument is an optional array of dependencies. If the dependencies are not provided, the callback will be triggered after every rendering of the functional component. However, if dependencies are specified, the callback will only run when the values inside the dependencies change.
useEffect is triggered after every component render to check dependencies and execute.
Example
The following component makes a request to retrieve the article content when the ID changes and displays it.
import React, { useState, useEffect } from "react";
function BlogView({ id }) {
// Define a local state to store the blog content
const [blogContent, setBlogContent] = useState(null);
useEffect(() => {
// Wrap the useEffect callback with a non-async function
const doAsync = async () => {
// Clear the current content to maintain consistency when id changes
setBlogContent(null);
// Fetch the data
const res = await fetch(`/blog-content/${id}`);
// Update the state with the fetched data
setBlogContent(await res.text());
};
doAsync();
}, [id]); // Use id as a dependency, and run the effect when id changes
// Check if the component is in a loading state, if there is no blogContent
const isLoading = !blogContent;
return <div>{isLoading ? "Loading..." : blogContent}</div>;
}
We used useEffect to fetch data when the ID of a Blog article changes. This is simpler than using componentDidUpdate in class components to check for changes in ID and make the request only if needed.
Two special uses
useEffect has two special uses: no dependencies and an empty array as dependencies. Let's take a closer look.
- If there are no dependencies, then the effect will be executed after each render. For example:
import React, { useEffect } from "react";
function ExampleComponent() {
useEffect(() => {
// This effect will run every time the component is re-rendered
console.log('re-rendered');
});
return <div>Example component</div>;
}
- When an empty array is provided as the dependencies, the effect will only be triggered on the first render, which corresponds to
componentDidMountin Class components.
useEffect(() => {
// Only executed on the first render, equivalent to componentDidMount in a class component
console.log('did mount');
}, []);
useEffect also allows you to return a function that will be executed when the component is unmounted, allowing you to perform cleanup operations, such as removing event listeners. This mechanism is equivalent to the componentWillUnmount method in class components.
Here's an example of using useEffect in a component to listen for changes in the size of the window for layout adjustments:
// Set a state "size" to store the current window size
const [size, setSize] = useState({});
useEffect(() => {
// Handler function for the window size change event
const handler = () => {
setSize(getSize());
};
// Listen for the "resize" event
window.addEventListener('resize', handler);
// Return a callback that will be called when the component is unmounted
return () => {
// Remove the "resize" event
window.removeEventListener('resize', handler);
};
}, []);
- After every render: Do not provide a second dependency parameter. For example,
useEffect(() => {}). - Only after the first render: Provide an empty array as a dependency. For example,
useEffect(() => {}, []). - Executed after the first render and when dependencies change: Provide a dependency array. For example,
useEffect(() => {}, [deps]). - Executed after component unmounts: Return a callback function. For example,
useEffect(() => { return () => {} }, [])
Understanding Dependencies of Hooks
Hooks allow you to listen for changes in certain data. This change could trigger a component refresh, create a side effect, or refresh a cache. The mechanism for defining what data changes to listen for is specifying the dependencies for the Hooks.
The dependencies are not a special mechanism of Hooks, but can be considered a design pattern. Hooks with similar requirements can be implemented using this pattern.
When defining dependencies, keep in mind:
- The variables listed in the dependencies must be used in the callback function, otherwise declaring them as dependencies is redundant.
- The dependencies usually consist of a constant array and not a variable, since you should be clear about which dependencies will be used in the callback.
- React uses shallow comparison to compare dependencies to see if they have changed. So be careful with array or object types. If you create a new object every time, even if it is equivalent to the previous value, it will be considered a change in the dependency. This is an easy place to cause bugs when first using Hooks. For example, the code below:
function Sample() {
// A new array is created every time the component is executed
const todos = [{ text: 'Learn hooks.'}];
useEffect(() => {
console.log('Todos changed.');
}, [todos]);
}
The code's intention might be to generate some side effects when the todos change, but the todos variable is created inside the function and actually creates a new array every time. So when comparing the references as dependencies, it is actually considered to have changed.
Mastering the usage rules of Hooks.
Hooks are pure JavaScript functions defined directly and not created through a special API. They need to follow certain rules to work properly while reducing the cost of learning and use. The rules of using Hooks include the following two points:
- they can only be used in the top-level scope of a functional component
- they can only be used in functional components or other Hooks.
Top-level scope of a functional component
Hooks must be used at the top level of a function component and must be called in the same order on each render. They cannot be used within loops, conditions or nested functions. This is because React maintains a fixed list of Hooks for each component, in order to maintain state between renders and make comparisons. For example, the following code is valid because the Hooks are guaranteed to be executed:
import { useState } from 'react';
function MyComp() {
// Declare a state variable called "count" and initialize it to 0
const [count, setCount] = useState(0);
// ...
// Render the value of "count" inside a div
return <div>{count}</div>;
}
The following code is incorrect because Hooks may not be executed under certain conditions.
function MyComp() {
const [count, setCount] = useState(0);
if (count > 10) {
// Error: Can't use Hook inside conditionals
useEffect(() => {
// ...
}, [count])
}
// There may be early return of component rendering result, then no more Hooks can be used
if (count === 0) {
return 'No content';
}
// Error: Can't use Hook after possible return statement
const [loading, setLoading] = useState(false);
//...
return <div>{count}</div>
}
Hooks rule can be summarized as two points:
- first, all Hooks must be executed;
- second, they must be executed in order.
Hooks can only be used in function components or other Hooks.
Hooks can only be used in function components or custom Hooks.
This rule can cause trouble in projects that have both function and class components, as Hooks are simple and intuitive and we might tend to use them for logic reuse. However, if you have to use Hooks in class components, you can wrap them in a higher-order component to make them usable in class components.
For example, if we have already defined a hook, useWindowSize, to listen to window size changes, it can easily be converted into a Higher-Order Component.
import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';
// HOC to wrap component with window size hook
export const withWindowSize = (Comp) => {
// Return component that passes window size hook as prop
return props => {
const windowSize = useWindowSize();
return <Comp windowSize={windowSize} {...props} />;
};
};
So we can use the Higher Order Component with the following code:
import React from 'react';
import { withWindowSize } from './withWindowSize';
// Define the class component
class MyComp extends React.Component {
render() {
const { windowSize } = this.props;
// Render component with windowSize prop
// ...
}
}
// Enhance MyComp with the withWindowSize HOC
export default withWindowSize(MyComp);
Using the ESLint plugin to help check the usage of Hooks.
When applied to daily development, you must always pay attention to not making mistakes.
- variables used in the callback function of useEffect must be declared in dependencies;
- Hooks cannot appear in condition statements or loops and cannot appear after return;
- Hooks can only be used in functional components or custom Hooks.
You may wonder, to ensure complete compliance with the rules, it seems difficult and you must be very careful, do you know any good ways to help master it? The thoughtful React official has provided us with an ESLint plugin specifically for checking the correct use of Hooks, which is the eslint-plugin-react-hooks.
Installing the plugin is simple
yarn add eslint-plugin-react-hooks --dev
n the ESLint configuration file, add two rules: rules-of-hooks and exhaustive-deps. As follows:
{
"plugins": [
// ...
// add react-hooks plugin
"react-hooks"
],
"rules": {
// ...
// check the rules of using Hooks
"react-hooks/rules-of-hooks": "error",
// check declaration of dependencies
"react-hooks/exhaustive-deps": "warn"
}
}
Summary
We discussed the use of two essential hooks, useState and useEffect.
The former saves state, while the latter handles side effects.
With these hooks, we can handle the majority of React development tasks. We also learned about hook dependencies and usage guidelines through examples.
By defining different dependencies, we can execute different actions during different component lifecycle phases. It is crucial to understand these concepts as they apply to all other built-in and custom hooks, making future hook use easier.
This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Yusen Meng
Yusen Meng | Sciencx (2023-02-04T02:54:48+00:00) Built-in Hooks (1): How to Preserve Component State and Use LifeCycle?. Retrieved from https://www.scien.cx/2023/02/04/built-in-hooks-1-how-to-preserve-component-state-and-use-lifecycle/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.