REACT-olution : Understanding React-Hooks one step at a time
REACT-olution : Understanding React-Hooks one step at a time

REACT-olution : Understanding React-Hooks one step at a time

Tags
React.js
frontend
Projects
Software Development
Web Dev
Computer Science
Parent item
Sub-item
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?

How to declare and use the useReducer hook

  • Here is an example of how to declare and use the useReducer hook:
    • Idea IconTheTechCruise.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

Advantages and limitations

Extrapolating with To-Do App:

Idea IconTheTechCruise.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;
 
Buy us a coffeeBuy us a coffee