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
orsetState
. 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 thestatic contextType
property. We can also update context values from function components without using theProvider
component or thethis.context
property.
- Refs: With Hooks, we can create and access refs in function components without using the
createRef
orforwardRef
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 thesetData
function. ThesetData
function is called in two places: inside thefetchData
function, which sets the data to the response content from the API, and inside thehandleDeleteClick
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 thesetLoading
function. ThesetLoading
function is called in two places: inside thefetchData
function, which sets the loading to true before making the API call and to false after receiving the response, and inside theuseEffect
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 thesetCurrentPage
function. ThesetCurrentPage
function is called in one place: inside thehandlePageChange
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 thesetItemsPerPage
function. ThesetItemsPerPage
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 thesetReadOnly
function. ThesetReadOnly
function is called in two places: inside thehandleModalClose
function, which sets the readonly to false when the modal closes, and inside thehandleExpandClick
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 thesetIsModalOpen
function. ThesetIsModalOpen
function is called in four places: inside thehandleEditClick
function, which sets the modal open to true when a todo item is edited, inside thehandleModalClose
function, which sets the modal open to false when the modal closes, inside thehandleExpandClick
function, which sets the modal open to true when a todo item is expanded, and inside thehandleFormSubmit
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 thesetSelectedCardData
function. ThesetSelectedCardData
function is called in two places: inside thehandleEditClick
function, which sets the selected card data to the item received from the event, and inside thehandleExpandClick
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 thesetSnackbarOpen
function. ThesetSnackbarOpen
function is called in three places: inside thehandleFormSubmit
function, which sets the snackbar open to true after submitting or failing to submit the form data, inside thehandleSnackbarClose
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 thesetSnackbarMessage
function. ThesetSnackbarMessage
function is called in two places: inside thehandleFormSubmit
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 thesetSnackbarSeverity
function. ThesetSnackbarSeverity
function is called in two places: inside thehandleFormSubmit
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:
TheTechCruise.com Pyodide Terminal
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:
TheTechCruise.com Pyodide Terminal
// 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 asdata
,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 asSET_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 isSET_DATA
, it returns a new state object that has thedata
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 theTodoCardLayout
component, and it takes thereducer
function and theinitialState
object as arguments. It returns an array with two values: thestate
and thedispatch
function12. Thestate
is the current state object that holds the state properties, and thedispatch
function is used to update the state by passing an action object12.
- The
state
object is destructured to get the individual state properties, such asdata
,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 calldispatch({ type: 'SET_LOADING', payload: true })
.
- The
fetchData
function is an async function that takes thedispatch
function and thestate
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, thehandlePageChange
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.