React Hooks Best Practices

React Hooks Best Practices

·

7 min read

React Hooks were introduced in React 16.8 and have since become a popular way to manage state and side effects in functional components.

Only call Hooks at the top level

Hooks should only be called from the top level of a function component, not inside loops, conditions, or nested functions. This ensures that Hooks are called in the same order each time the component is rendered, which is essential for the consistency of your component's behaviour.

Here are some best practices to follow when using React Hooks:

useEffect Hook

  1. Use the useEffect Hook for side effects: The useEffect Hook allows you to perform side effects in functional components, such as fetching data or setting up a subscription. It should be used instead of lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.

Below is an example of using the useEffect Hook to fetch data in a functional component:

import { useEffect, useState } from 'react';
//assuming the type of response. This defines Response as an object with three properties: an id of type number, a name of type string, and a description of type string.
type Response = {
  id: number,
  name: string,
  description: string
}

function ExampleComponent() {
  const [data, setData] = useState<Response | null>(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('http://my-api.com/data');
      const json = await response.json();
      setData(json);
    }
    fetchData();
  }, []);

  return (
    <div>
      {data ? data.map((item: Response) => <div key={item.id}>{item.name}</div>) : <div>Loading...</div>}
    </div>
  );
}

useRef Hook

  1. useRef Hook: The useRef Hook is a useful way to create a reference to a DOM element or a value in a functional component. Here are some best practices to follow when using useRef:

    1. Use useRef for values that don't need to trigger a re-render: useRef is a good choice for storing values that don't need to trigger a re-render when they change, such as a timer ID or a DOM element reference.

    2. Avoid using useRef for values that should trigger a re-render: Instead of using useRef, consider using a state variable or the useState Hook to store values that should trigger a re-render when they change.

    3. Use useRef to keep a value between renders: The value stored in an useRef object will persist between renders, so it can be used to keep track of values that need to be preserved between renders.

Here is an example of using useRef to store a reference to a DOM element in a functional component


import { useRef, useEffect, HTMLInputElement } from 'react';

function RefComponent() {
  const inputEl = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputEl.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputEl} type="text" />
    </div>
  );
}

In this example, the useRef Hook is used to create a reference to an input element. The useEffect Hook is then used to focus on the input element when the component is rendered. The type annotation for inputEl indicates that it is a reference to an HTMLInputElement.

useReducer Hook

  1. The useReducer Hook is a useful way to manage state in a functional component, especially when the state updates require complex logic or when the state updates are related to each other. Here are some best practices to follow when using useReducer:

    1. Use useReducer for complex state updates: useReducer is a good choice for managing state updates that require complex logic, such as updating an array or an object, or for managing related state updates.

    2. Use a switch statement in the reducer function: The reducer function should handle each action type with a separate case in a switch statement. This makes it easier to read and maintain the reducer function.

    3. Use action creators to create action objects: Consider using action creators to create action objects, which can help to make the reducer function more readable and maintainable.

    4. Use types and generics to improve the type safety of your useReducer implementation. This can help to catch errors and prevent bugs in your code.

Here is an example of using useReducer to fetch and display a todo list in a functional component:

import { useReducer, useEffect } from 'react';

type Todo = {
  id: number,
  name: string,
  description: string,
  completed: boolean
};

type TodoState = {
  todos: Todo[],
  loading: boolean,
  error: string | null
};

type TodoAction =
  | { type: 'FETCH_TODOS_REQUEST' }
  | { type: 'FETCH_TODOS_SUCCESS', todos: Todo[] }
  | { type: 'FETCH_TODOS_ERROR', error: string };

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'FETCH_TODOS_REQUEST':
      return { ...state, loading: true };
    case 'FETCH_TODOS_SUCCESS':
      return { todos: action.todos, loading: false, error: null };
    case 'FETCH_TODOS_ERROR':
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}

function fetchTodosRequest(): TodoAction {
  return { type: 'FETCH_TODOS_REQUEST' };
}

function fetchTodosSuccess(todos: Todo[]): TodoAction {
  return { type: 'FETCH_TODOS_SUCCESS', todos };
}

function fetchTodosError(error: string): TodoAction {
  return { type: 'FETCH_TODOS_ERROR', error };
}

function ReducerComponent() {
  const [state, dispatch] = useReducer(todoReducer, { todos: [], loading: false, error: null });

  useEffect(() => {
    dispatch(fetchTodosRequest());
    async function fetchData() {
      try {
        const response = await fetch('http://my-api.com/todos');
        const json = await response.json();
        dispatch(fetchTodosSuccess(json));
      } catch (error) {
        dispatch(fetchTodosError(error.message));
      }
    }
    fetchData();
  }, []);

  return (
    <div>
      {state.loading && <div>Loading...</div>}
      {state.error && <div>{state.error}</div>}
      {state.todos.map((todo: Todo) => (
        <div key={todo.id}>{todo.name}</div>
      ))}
    </div>
  );
}

In this example, the useReducer Hook is used to manage the state of the todo list, including the todos, loading status, and error status. The reducer function handles the different action types to update the state accordingly. Action creators are used to create action objects that are dispatched to the reducer function. The useEffect Hook is used to fetch the todo list from the API and dispatch the appropriate actions

useContext Hook

  1. The useContext Hook is a useful way to consume context in a functional component. Here are some best practices to follow when using the useContext API:

    1. Use the createContext function to create a context: The createContext function takes a default value as an argument and returns a context object with a Provider and a Consumer component.

    2. Use the Provider component to provide values for the context: The Provider component should be used to provide values for the context, which can be consumed by the Consumer component or the useContext Hook.

    3. Use the useContext Hook to consume context in a functional component: The useContext Hook can be used to consume context in a functional component. It takes a context object as an argument and returns the current value of the context.

import React, { useReducer, createContext, useContext } from "react";

type Adapter = {
  id: string;
  name: string;
  adapter: () => void;
};

type AdapterState = {
  adapters: Adapter[];
};

type AdapterContextType = {
  state: AdapterState;
  addAdapter: (adapter: Adapter) => void;
  removeAdapter: (id: string) => void;
};

type AdapterAction =
  | { type: "ADD_ADAPTER"; adapter: Adapter }
  | { type: "REMOVE_ADAPTER"; id: string };

function adapterReducer(
  state: AdapterState,
  action: AdapterAction
): AdapterState {
  switch (action.type) {
    case "ADD_ADAPTER":
      return { adapters: [...state.adapters, action.adapter] };
    case "REMOVE_ADAPTER":
      return {
        adapters: state.adapters.filter((adapter) => adapter.id !== action.id),
      };
    default:
      return state;
  }
}

const AdapterContext = createContext<AdapterContextType>({
  state: { adapters: [] },
  addAdapter: () => {},
  removeAdapter: () => {},
});

function AdapterProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(adapterReducer, { adapters: [] });

  const addAdapter = (adapter: Adapter) => {
    dispatch({ type: "ADD_ADAPTER", adapter });
  };

  const removeAdapter = (id: string) => {
    dispatch({ type: "REMOVE_ADAPTER", id });
  };

  return (
    <AdapterContext.Provider value={{ state, addAdapter, removeAdapter }}>
      {children}
    </AdapterContext.Provider>
  );
}

function useAPIAdapters() {
  const { state, addAdapter, removeAdapter } = useContext(AdapterContext);
  const callAPIAdapters = () => {
    state.adapters.forEach((adapter) => {
      adapter.adapter();
    });
  };

  return { callAPIAdapters, addAdapter, removeAdapter };
}

function ContextComponent() {
  const { callAPIAdapters, addAdapter } = useAPIAdapters();

  const handleClick = () => {
    addAdapter({
      id: 'my-adapter-1',
      name: 'My Adapter',
      adapter: () => console.log('Calling my adapter')
    });
    callAPIAdapters();
  };

  return <button onClick={handleClick}>Call API Adapters</button>;
}

function App() {
  return (
    <AdapterProvider>
      <ContextComponent/>
    </AdapterProvider>
  );
}

export default App;

In this example, the AdapterContext is created using the createContext function, and the AdapterProvider component is used to provide values for the context. The useAPIAdapters hook is used to consume the context and provide functions for calling the API adapters and adding or removing adapters. The MyComponent component uses the useAPIAdapters hook to get the callAPIAdapters function, which it uses to call the API adapters when the button is clicked.

Conclusion

This is an extensive topic. The list of approaches and patterns is neither comprehensive nor definitive. The goal is rather to illustrate the thought process behind solving a specific problem in a particular way, the React Hooks way.

Happy coding! ✨

Keep Learning and practising, React Hooks.

If you have any questions, You can reach out to me on:

Twitter

LinkedIn

Did you find this article valuable?

Support Jay Desai by becoming a sponsor. Any amount is appreciated!