REACT-olution : Understanding React-Hooks one step at a time
React Hooks is a new feature introduced in React 16.8 that allows us to use state and other React features without writing a class component. Hooks are functions that let us "hook…

React Hooks is a new feature introduced in React 16.8 that allows us to use state and other React features without writing a class component. Hooks are functions that let us “hook into” the React state and lifecycle from a function component. They also make the code more readable and easier to maintain.
In this article, we will try to learn about React Hooks by looking at the progression and evolution from useState to useReducer to other hooks, eventually trying to cover a good majority of them.
However, to get started with this article, let us understand/answer the below questions -
- What are Hooks in React?
- Why do we need Hooks?
- What are the different kinds of Hooks in React?
What are Hooks in React?
Hooks are a way of using state and other React features in function components. Before Hooks, we had to use class components to access state, lifecycle methods, context, refs, etc. But class components can be verbose, complex, and hard to test. Hooks simplify the code and make it more functional.
Hooks are not a new concept. They are based on existing JavaScript features, such as closures and higher-order functions. They are also inspired by other libraries, such as React Router, Redux, and Apollo.
Hooks do not replace class components. They are an alternative way of writing components that work well with existing code. You can use Hooks in new components or gradually refactor your old ones.
Why do we need Hooks?
Hooks solve some common problems that we face when writing React components, such as:
- State management: With Hooks, we can use state in function components without using this or setState. We can also use multiple state variables and update them independently.
- Side effects: With Hooks, we can perform side effects (such as fetching data, subscribing to events, updating the document title, etc.) in function components without using lifecycle methods (such as componentDidMount, componentDidUpdate, etc.). We can also control when and how the side effects run by using dependencies and cleanup functions.
- Code reuse: With Hooks, we can extract common logic into custom hooks and reuse them across different components. We can also share state and behavior between components without using higher-order components or render props.
- Context: With Hooks, we can access context values in function components without using the Consumer component or the static contextType property. We can also update context values from function components without using the Provider component or the this.context property.
- Refs: With Hooks, we can create and access refs in function components without using the createRef or forwardRef APIs. We can also use refs for more than just accessing DOM nodes, such as storing mutable values or accessing child components.
What are the different kinds of Hooks in React?
React provides a few built-in hooks that cover the most common use cases, such as:
- useState: This hook lets us use state in function components. It returns a state variable and a function to update it.
- useEffect: This hook lets us perform side effects in function components. It runs after every render by default, but we can specify dependencies to control when it runs. It also accepts a cleanup function that runs before the next effect or when the component unmounts.
- useContext: This hook lets us access context values in function components. It accepts a context object and returns the current value of that context.
- useReducer: This hook lets us use reducer functions to manage complex state logic in function components. It returns a state variable and a dispatch function to update it.
- useCallback: This hook lets us memoize callback functions to prevent unnecessary re-renders of child components. It accepts a callback function and an array of dependencies and returns a memoized version of the callback.
- useMemo: This hook lets us memoize expensive computations to avoid repeating them on every render. It accepts a function and an array of dependencies and returns the memoized value of the function.
- useRef: This hook lets us create and access refs in function components. It accepts an initial value and returns a ref object with a current property that points to the current value of the ref.
- useImperativeHandle: This hook lets us customize the value that is exposed to parent components when using refs. It accepts a ref object and a function that returns the value to be assigned to the ref.current property.
- useLayoutEffect: This hook is similar to useEffect, but it runs synchronously after all DOM mutations. It is useful for reading layout values (such as scroll position or size) before the browser paints.
- useDebugValue: This hook is useful for displaying a label for custom hooks in React DevTools. It accepts a value or a format function that returns the value to be displayed.
In addition to these built-in hooks, we can also create our own custom hooks by combining the existing ones. Custom hooks are a powerful way of reusing stateful logic across different components.
In the next sections, we will explore some of the most commonly used hooks in more detail, starting with useState.
useState
What is useState?
useState is one of the fundamental hooks in React. It allows functional components to manage and update their local state. With useState, you can add state to your components, making them dynamic and responsive to user interactions.
How to declare and use the useState hook?
To use useState, you need to import it from the React library. Here’s a basic example of how to declare and use useState:
In this example, we initialize a state variable count with an initial value of 0 using useState. We also get a function setCount that allows us to update the count state.
Some important things to know about useState
- The argument passed to useState is the initial state value.
- useState returns an array with two elements: the current state value and a function to update it.
- State updates are asynchronous and may be batched for performance - this also means - when we try to update the state and then if we try to do a console.log of the state object, we might not see the updated value.
- You can call the state update function with a new value or a function that receives the previous state and returns a new state.
Advantages and limitations of useState
Advantages:
- Simple and intuitive for managing component-local state.
- Lightweight and easy to use, making it suitable for most scenarios.
Limitations:
- Not suitable for complex state management or global state sharing - we will get to see this in a bit.
- Handling multiple state variables can lead to “state explosion” in components.
Extrapolating with To-Do app
When we implement the same for our To-Do application from our previous blog, this is how the code looks like -
const TodoCardLayout = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [readonly, setReadOnly] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCardData, setSelectedCardData] = useState({});
const [snackbarOpen, setSnackbarOpen] = useState(false); // state for snackbar visibility
const [snackbarMessage, setSnackbarMessage] = useState(''); // state for snackbar message
const [snackbarSeverity, setSnackbarSeverity] = useState('success'); // state for snackbar severity
const fetchData = async () => {
setLoading(true);
const response = await getTodoSchemaOrAPI('api', 'list', { page: currentPage, size: itemsPerPage });
setData(response.content);
setLoading(false);
};
useEffect(() => {
fetchData();
}, [currentPage, itemsPerPage]);
const handlePageChange = (event, value) => {
setCurrentPage(value);
};
const handleEditClick = (item) => {
setSelectedCardData(item);
setIsModalOpen(true);
};
const handleDeleteClick = async (item) => {
const confirmDelete = window.confirm('Are you sure you want to delete this item?');
if (!confirmDelete) return;
await getTodoSchemaOrAPI('api', 'delete', { id: item.id });
const updatedData = data.filter((todo) => todo.id !== item.id);
setData(updatedData);
};
const handleModalClose = () => {
setIsModalOpen(false);
setReadOnly(false);
};
const handleExpandClick = (item) => {
setSelectedCardData(item);
setReadOnly(true);
setIsModalOpen(true);
};
const handleFormSubmit = async (formData) => {
try {
// try to update the data
await getTodoSchemaOrAPI('api', 'update', { id: formData.id, object: formData });
// if successful, close the modal and show a success snackbar
setIsModalOpen(false);
setSnackbarMessage('Todo updated successfully');
setSnackbarSeverity('success');
setSnackbarOpen(true);
} catch (error) {
// if error, close the modal and show an error snackbar
setIsModalOpen(false);
setSnackbarMessage('Todo update failed');
setSnackbarSeverity('error');
setSnackbarOpen(true);
}
};
// handle snackbar close
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbarOpen(false);
};
return (
<div className="container">
<h1>Card Layout</h1>
<CardLayout data={data} loading={loading} handleEditClick={handleEditClick} handleDeleteClick={handleDeleteClick} handleExpandClick={handleExpandClick} />
<Dialog open={isModalOpen} onClose={handleModalClose}>
<DialogTitle>Edit Todo</DialogTitle>
<DialogContent>
<ErrorBoundary>
<RSJFForm schema={todoSchema} formData={selectedCardData} uiSchema={uiSchema} onSubmit={handleFormSubmit} readonly={readonly} noValidate={true} />
</ErrorBoundary>
</DialogContent>
</Dialog>
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
<Alert onClose={handleSnackbarClose} severity={snackbarSeverity} sx={{ width: '100%' }}>
{snackbarMessage}
</Alert>
</Snackbar>
</div>
);
};
export default TodoCardLayout;Here is a summary of what each state object does and how it is updated:
- data: This is an array of todo items that are fetched from an API. It is initialized as an empty array and updated by the setData function. The setData function is called in two places: inside the fetchData function, which sets the data to the response content from the API, and inside the handleDeleteClick function, which sets the data to a filtered array that excludes the deleted item.
- loading: This is a boolean value that indicates whether the data is being fetched or not. It is initialized as false and updated by the setLoading function. The setLoading function is called in two places: inside the fetchData function, which sets the loading to true before making the API call and to false after receiving the response, and inside the useEffect hook, which sets the loading to false when the component mounts.
- currentPage: This is a number that represents the current page of the data pagination. It is initialized as 0 and updated by the setCurrentPage function. The setCurrentPage function is called in one place: inside the handlePageChange function, which sets the current page to the value received from the event.
- itemsPerPage: This is a number that represents the number of items to display per page. It is initialized as 10 and updated by the setItemsPerPage function. The setItemsPerPage function is not called anywhere in your code, but you can use it to change the items per page dynamically if you want.
- readonly: This is a boolean value that indicates whether the modal form is read-only or editable. It is initialized as false and updated by the setReadOnly function. The setReadOnly function is called in two places: inside the handleModalClose function, which sets the readonly to false when the modal closes, and inside the handleExpandClick function, which sets the readonly to true when a todo item is expanded.
- isModalOpen: This is a boolean value that indicates whether the modal is open or closed. It is initialized as false and updated by the setIsModalOpen function. The setIsModalOpen function is called in four places: inside the handleEditClick function, which sets the modal open to true when a todo item is edited, inside the handleModalClose function, which sets the modal open to false when the modal closes, inside the handleExpandClick function, which sets the modal open to true when a todo item is expanded, and inside the handleFormSubmit function, which sets the modal open to false after submitting or failing to submit the form data.
- selectedCardData: This is an object that holds the data of the selected todo item. It is initialized as an empty object and updated by the setSelectedCardData function. The setSelectedCardData function is called in two places: inside the handleEditClick function, which sets the selected card data to the item received from the event, and inside the handleExpandClick function, which sets the selected card data to the item received from the event.
- snackbarOpen: This is a boolean value that indicates whether the snackbar is open or closed. It is initialized as false and updated by the setSnackbarOpen function. The setSnackbarOpen function is called in three places: inside the handleFormSubmit function, which sets the snackbar open to true after submitting or failing to submit the form data, inside the handleSnackbarClose function, which sets the snackbar open to false when it closes, and inside the useEffect hook, which sets it to false when component mounts.
- snackbarMessage: This is a string that holds the message of the snackbar. It is initialized as an empty string and updated by the setSnackbarMessage function. The setSnackbarMessage function is called in two places: inside the handleFormSubmit function, which sets the snackbar message to a success or error message depending on the outcome of the form submission, and inside the useEffect hook, which sets it to an empty string when component mounts.
- snackbarSeverity: This is a string that holds the severity of the snackbar. It can be one of ‘success’, ‘error’, ‘warning’, or ‘info’. It is initialized as ‘success’ and updated by the setSnackbarSeverity function. The setSnackbarSeverity function is called in two places: inside the handleFormSubmit function, which sets the snackbar severity to ‘success’ or ‘error’ depending on the outcome of the form submission, and inside the useEffect hook, which sets it to ‘success’ when component mounts.
As we could see here, although we are able to preserve the state of the application using the useState hook, we can notice that there is a lot of complexity here in managing various objects.
And we’ve not even added validations, integrity checks amongst other things.
This is the challenge we have when we maintain a bunch of state objects. To do better we might want to understand the alternatives and here in this situation a useReducer rightly fits in. So let us understand the bits and pieces of useReducer.
useReducer
What is useReducer?
- useReducer is a React hook that lets you manage complex state logic in your components. It is similar to the Redux pattern, where you have a store, a reducer, and an action. The store holds the state data, the reducer is a function that updates the state based on the action, and the action is an object that describes how to change the state12.
How to declare and use the useReducer hook
- To use the useReducer hook, you need to import it from React and call it at the top level of your component or your custom hook. You need to pass two arguments to useReducer: a reducer function and an initial state. Optionally, you can also pass a third argument: an initializer function that returns the initial state13.
- useReducer returns an array with two values: the current state and a dispatch function. You can use the state value to render your component, and you can use the dispatch function to update the state by passing an action object13.
- Here is an example of how to declare and use the useReducer hook:
import React, { useReducer } from 'react';
// Define the initial state
const initialState = { count: 0 };
// Define the reducer function
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
// Define the component that uses the useReducer hook
function Counter() {
// Call the useReducer hook and get the state and dispatch values
const [state, dispatch] = useReducer(reducer, initialState);
// Render the component using the state value
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
Some important things to know about useReducer
- You should only call useReducer at the top level of your component or your custom hook. You should not call it inside loops or conditions, as this might break the rules of hooks12.
- The reducer function should be pure, meaning it should not have any side effects or depend on any external variables. It should only take the state and action as arguments, and return the next state12.
- The action object can be of any type, but it is recommended to have a type property that describes what kind of action it is. You can also have other properties that carry some payload or data for the reducer12.
- The dispatch function is used to trigger a state update by passing an action object. React will call the reducer function with the current state and the action object, and set the next state to the return value of the reducer12.
- In strict mode, React will call your reducer and initializer twice in order to help you find accidental impurities. This is only for development purposes and does not affect production. If your reducer and initializer are pure, this should not affect your logic1.
Advantages and limitations
- One of the advantages of using useReducer is that it allows you to separate the state management logic from the rendering logic of your component. This can make your code more readable and maintainable, especially for complex components that have multiple substates and transitions23.
- Another advantage of using useReducer is that it can be combined with other hooks like useContext to create a global state management system that is similar to Redux, but without using any external libraries. This can be useful for small to medium-sized applications that do not need all the features and complexity of Redux24.
- One of the limitations of using useReducer is that it is tightly coupled to a specific reducer function. You can only dispatch actions to that reducer, whereas in Redux, you can dispatch actions to multiple reducers through a single store. This means that you might need to create multiple useReducer instances for different parts of your state, or use some techniques like combining reducers or creating custom hooks24.
- Another limitation of using useReducer is that it does not provide any built-in tools for debugging, testing, or middleware. You might need to implement your own solutions or use some third-party libraries for these purposes. For example, you might want to use the React Developer Tools extension, the useReducerLogger library, or the React Testing Library24.
Extrapolating with To-Do App:
// Create a context to manage state
const TodoContext = React.createContext();
const initialState = {
data: [],
loading: false,
currentPage: 0,
itemsPerPage: 10,
readonly: false,
isModalOpen: false,
selectedCardData: {},
snackbarOpen: false,
snackbarMessage: '',
snackbarSeverity: 'success',
};
function reducer(state, action) {
switch (action.type) {
case 'SET_DATA':
return { ...state, data: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_CURRENT_PAGE':
return { ...state, currentPage: action.payload };
case 'SET_SELECTED_CARD_DATA':
return { ...state, selectedCardData: action.payload };
case 'SET_IS_MODAL_OPEN':
return { ...state, isModalOpen: action.payload };
case 'SET_READONLY':
return { ...state, readonly: action.payload };
case 'SET_SNACKBAR_OPEN':
return { ...state, snackbarOpen: action.payload };
case 'SET_SNACKBAR_MESSAGE':
return { ...state, snackbarMessage: action.payload };
case 'SET_SNACKBAR_SEVERITY':
return { ...state, snackbarSeverity: action.payload };
default:
return state;
}
}
const fetchData = async (dispatch, state) => {
dispatch({ type: 'SET_LOADING', payload: true });
const response = await getTodoSchemaOrAPI('api', 'list', {
page: state.currentPage,
size: state.itemsPerPage,
});
if (response.content.length > 0){
dispatch({ type: 'SET_DATA', payload: response.content });
dispatch({ type: 'SET_LOADING', payload: false });
}
else {
}
};
const TodoCardLayout = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { data, loading, currentPage, itemsPerPage, readonly, isModalOpen, selectedCardData, snackbarOpen, snackbarMessage, snackbarSeverity } = state;
const handlePageChange = (event, value) => {
dispatch({ type: 'SET_CURRENT_PAGE', payload: value });
};
const handleEditClick = (item) => {
dispatch({ type: 'SET_SELECTED_CARD_DATA', payload: item });
dispatch({ type: 'SET_IS_MODAL_OPEN', payload: true });
};
const handleDeleteClick = useCallback(async (item) => {
const confirmDelete = window.confirm('Are you sure you want to delete this item?');
if (!confirmDelete) return;
await getTodoSchemaOrAPI('api', 'delete', { id: item.id });
const updatedData = data.filter((todo) => todo.id !== item.id);
dispatch({ type: 'SET_DATA', payload: updatedData });
}, [data]);
const handleModalClose = () => {
dispatch({ type: 'SET_IS_MODAL_OPEN', payload: false });
dispatch({ type: 'SET_READONLY', payload: false });
};
const handleExpandClick = (item) => {
dispatch({ type: 'SET_SELECTED_CARD_DATA', payload: item });
dispatch({ type: 'SET_READONLY', payload: true });
dispatch({ type: 'SET_IS_MODAL_OPEN', payload: true });
};
const handleFormSubmit = async (formData) => {
try {
await getTodoSchemaOrAPI('api', 'update', { id: formData.id, object: formData });
dispatch({ type: 'SET_IS_MODAL_OPEN', payload: false });
dispatch({ type: 'SET_SNACKBAR_MESSAGE', payload: 'Todo updated successfully' });
dispatch({ type: 'SET_SNACKBAR_SEVERITY', payload: 'success' });
dispatch({ type: 'SET_SNACKBAR_OPEN', payload: true });
} catch (error) {
dispatch({ type: 'SET_IS_MODAL_OPEN', payload: false });
dispatch({ type: 'SET_SNACKBAR_MESSAGE', payload: 'Todo update failed' });
dispatch({ type: 'SET_SNACKBAR_SEVERITY', payload: 'error' });
dispatch({ type: 'SET_SNACKBAR_OPEN', payload: true });
}
};
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
dispatch({ type: 'SET_SNACKBAR_OPEN', payload: false });
};
useEffect(() => {
fetchData(dispatch, state);
}, [currentPage, itemsPerPage]);
return (
<TodoContext.Provider value={dispatch}>
<div className="container">
<h1>Card Layout</h1>
<CardLayout data={data} loading={loading} handleEditClick={handleEditClick} handleDeleteClick={handleDeleteClick} handleExpandClick={handleExpandClick} />
<Dialog open={isModalOpen} onClose={handleModalClose}>
<DialogTitle>Edit Todo</DialogTitle>
<DialogContent>
<ErrorBoundary>
<RSJFForm schema={todoSchema} formData={selectedCardData} uiSchema={uiSchema} onSubmit={handleFormSubmit} readonly={readonly} noValidate={true} />
</ErrorBoundary>
</DialogContent>
</Dialog>
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
<Alert onClose={handleSnackbarClose} severity={snackbarSeverity} sx={{ width: '100%' }}>
{snackbarMessage}
</Alert>
</Snackbar>
</div>
</TodoContext.Provider>
);
};
export default TodoCardLayout;- The initialState is an object that defines the initial values for the state properties. It contains properties such as data, loading, currentPage, etc. that are relevant for the todo card layout component12.
- The reducer is a function that takes the current state and an action object as arguments, and returns the next state based on the action type and payload12. It uses a switch statement to handle different action types, such as SET_DATA, SET_LOADING, SET_CURRENT_PAGE, etc. For each case, it returns a new state object that has the updated property value from the action payload. For example, if the action type is SET_DATA, it returns a new state object that has the data property set to the action payload. It also uses the spread operator (…) to copy the rest of the state properties that are not changed by the action12.
- The useReducer hook is called inside the TodoCardLayout component, and it takes the reducer function and the initialState object as arguments. It returns an array with two values: the state and the dispatch function12. The state is the current state object that holds the state properties, and the dispatch function is used to update the state by passing an action object12.
- The state object is destructured to get the individual state properties, such as data, loading, currentPage, etc. These properties are used to render the component or pass them to other functions or components12.
- The dispatch function is used to update the state by passing an action object that has a type and a payload. The type describes what kind of state change should happen, and the payload carries some data or information for the state update12. For example, to set the loading state to true, you can call dispatch({ type: ‘SET_LOADING’, payload: true }).
- The fetchData function is an async function that takes the dispatch function and the state object as arguments. It uses the dispatch function to set the loading state to true before making an API call to get some data. It then checks if the response has some content, and if so, it dispatches another action to set the data state to the response content and set the loading state to false12.
- The handlePageChange, handleEditClick, handleDeleteClick, etc. are event handler functions that are triggered by some user interactions, such as clicking a button or changing a page. They use the dispatch function to update some state properties based on the event or some logic. For example, the handlePageChange function dispatches an action to set the current page state to the value from the event12.
- The useEffect hook is used to call the fetchData function whenever the currentPage or itemsPerPage state changes. This ensures that the data is fetched from the API according to the pagination parameters12.
- The <TodoContext.Provider> component is used to provide the dispatch function as a value to its children components. This allows them to access and use the dispatch function without passing it as a prop explicitly13.