Mastering React Fundamentals: useState, useEffect, and useContext Explained
Hey, Have you ever thought about why someone would use React hooks? Hooks provide a more straightforward way to write components and manage states, making code more readable and maintainable. Hooks are nothing but functions that let you “hook into” React state and lifecycle features from function components
. Let’s have a look at the three most commonly used React hooks: useState, useEffect, useContext.
1. useState Hook
The useState hook is a fundamental React hook that allows you to add state to functional components. By calling useState, you can create a state variable and a function to update it. This hook returns an array with two elements: the current state value and a function that lets you update this value.
const [state, setState] = useState(initialState);
You can understand this with the counter example below.
import React, { useState } from 'react';
import Button from '@mui/material/Button';
function App() {
const [count, setCount] = useState(0);
const countUp = () => {
setCount(count + 1);
}
return (
<div>
<Button variant="contained" color="primary" onClick={countUp}>
Count Up
</Button>
<p>{count}</p>
</div>
);
}
export default App;
This is a simple counter (counting up) created with useState hook. The state variable is count and the function to update it is setCount function. When the button is clicked, we execute the countUp function, in which the setCount function is called. When the button is clicked, count is incremented by 1 each time.
Drawbacks
useState is ideal for simple state values, but managing complex state objects or multiple state values can lead to verbose code. Using multiple useState hooks in a single component, especially with complex objects or arrays, can lead to performance bottlenecks. Each state update triggers a re-render of the component, which might be inefficient for components with heavy rendering or large trees of child components.
When relying on the previous state to compute the next state, useState does not guarantee synchronous state updates due to React's asynchronous nature of setting state. This can lead to stale state issues, especially in rapid state update scenarios. React provides a functional update form (setState(prevState => newState)) to address this, but it requires additional care and understanding to use correctly.
2. useEffect Hook
The useEffect hook in React is a powerful tool that lets you perform side effects in functional components. useEffect is used for various tasks such as fetching data, directly updating the DOM, setting up subscriptions or timers, and cleaning up resources to prevent memory leaks. useEffect accepts two arguments. The second argument is optional.
useEffect(<function>, <dependency>)
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]); // State to store users
useEffect(() => {
// Function to fetch user data
const fetchUsers = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
setUsers(data); // Update state with fetched users
} catch (error) {
console.error("Failed to fetch users:", error);
}
};
fetchUsers(); // Call the fetch function
}, []); // Empty dependency array means this effect runs once after the initial render
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li> // Render each user's name in a list item
))}
</ul>
</div>
);
}
export default UserList;
Here, the function inside useEffect fetches data and updates the users state. The dependency list is empty, which means this useEffect is executed only in the initial render. So, fetching is happening only once. Be careful, when you define the dependency list. Have a look at the three scenarios below.
useEffect(() => {
//Runs only on the first render
}, []);
useEffect(() => {
//Runs on the first render
//And any time any dependency value changes
}, [prop, state]);
useEffect(() => {
//Runs on every render
});
Be careful:
In asynchronous operations, such as data fetching within useEffect, it's possible to encounter race conditions where the response of an earlier request arrives after a later request. Handling these conditions requires additional logic to check for the relevancy of responses, adding complexity to the component.
3. useContext Hook
With this hook, you can easily manage the states between components. Think there are four nested React components. You have one state in Component1 and you want to use that state inside Component4. Are you going to pass it for each child component?
function Component1() {
const [name, setName] = useState("Sumudu Liyanage");
return (
<>
<h1>{`Hello ${name}!`}</h1>
<Component2 name={name} />
</>
);
}
function Component2({ name }) {
return (
<>
<h1>Component 2</h1>
<Component3 name={name} />
</>
);
}
function Component3({ name }) {
return (
<>
<h1>Component 3</h1>
<Component4 name={name} />
</>
);
}
function Component4({ name }) {
return (
<>
<h1>Component 4</h1>
<Component5 user={name} />
</>
);
}
Is this how you are going to do it? This is where useContext comes in. You can achieve this as follows.
export const NameContext = createContext();
export default function Component1() {
const name = "Sara";
return (
<div>
<h1>App: {name}</h1>
<NameContext.Provider value={name}>
<Component2/>
</NameContext.Provider>
</div>
);
}
export default function Component2() {
return (
<div>
<h1>Component 2: </h1>
<Component3/>
</div>
);
}
export default function Component3() {
const name = useContext(NameContext);
return (
<div>
<h1>Component 3: {name}</h1>
</div>
);
}
So, In this blog, I described to you some of the most important hooks in React!
Happy Coding with React Hooks!