Paulund
2024-04-15 #ReactJS

React Hooks

useState

The useState hook is used to add state to a function component. This hook must be called from inside a component or another hook.

import { useState } from 'react';

const [state, setState] = useState(initialState);

The useState hook returns an array with two values. The first value is the current state and the second value is a function that you can use to update the state. The initialState is the initial value of the state.

This hook should be used when you want to add state to a function component. For example if you want to add a counter to a component you can use the useState hook.

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

If you want to add a form to a component you can use the useState hook to store the form values.

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
    </form>
  );
}

If you want to add a toggle to a component you can use the useState hook to store the toggle value.

function Toggle() {
  const [isToggled, setIsToggled] = useState(false);

  return (
    <div>
      <h1>{isToggled ? 'ON' : 'OFF'}</h1>
      <button onClick={() => setIsToggled(!isToggled)}>Toggle</button>
    </div>
  );
}

The useState hook can only be used on client side components. If you want to add state to a server side component you can use the useState hook in a custom hook and then use the custom hook in the server side component.

function useCounter() {
  const [count, setCount] = useState(0);
  return [count, setCount];
}

useEffect

The useEffect hook is used to watch for external changes and run side effects. This hook must be called from inside a component or another hook.

import { useEffect } from 'react';

useEffect(() => {
  // Run side effect
  return () => {
    // Clean up
  };
}, [dependencies]);

The first parameter is the side effect that you want to run and the second parameter is an array of dependencies. If any of the dependencies change the side effect will be re-run.

This hook should be used when you want to run side effects in function components. For example if you want to fetch data from an API when the component mounts you can use the useEffect hook.

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then((data) => setUser(data));
  }, [userId]);

  return <h1>{user.name}</h1>;
}

An external change is anything that is outside of react this can be another system or a browser API. For example if you want to display a message when users are offline you can use a browser API to check if the user is online.

function Status() {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  }, [friendID]);

  return isOnline;
}

If you want to set a function on a timer you can use the useEffect hook to set the timer when the component mounts and clear the timer when the component unmounts.

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

  return <h1>{count}</h1>;
}

If you want to response to a window mouse event you would use useEffect to add the event listener when the component mounts and remove the event listener when the component unmounts.

function Mouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMouseMove(event) {
      setPosition({
        x: event.clientX,
        y: event.clientY,
      });
    }

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return <h1>{position.x}, {position.y}</h1>;
}

useLayoutEffect

The useLayoutEffect hook is similar to the useEffect hook but it runs synchronously after the DOM has been updated. This hook must be called from inside a component or another hook.

import { useLayoutEffect } from 'react';

useLayoutEffect(() => {
  // Run side effect
  return () => {
    // Clean up
  };
}, [dependencies]);

The first parameter is the side effect that you want to run and the second parameter is an array of dependencies. If any of the dependencies change the side effect will be re-run.

This hook should be used when you want to run side effects that need to be run synchronously after the DOM has been updated. For example if you want to measure the size of an element after it has been rendered you can use the useLayoutEffect hook.

function Measure() {
  const [height, setHeight] = useState(0);

  const measuredRef = useLayoutEffect((node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div ref={measuredRef}>
      <h1>Height: {height}</h1>
    </div>
  );
}

useContext

This useContext hook is used to read a context value from the nearest provider for the context. The useContext hook accepts a context object and returns the current context value for that context.

import { useContext } from 'react';

const value = useContext(MyContext);

The MyContext is the the provider that you want to read the value from. The useContext hook will return the current context value for that context. If the context value changes then the component will re-render. To create a provider you can define this on a component.

const MyContext = React.createContext(defaultValue);

Then you can use the MyContext.Provider to provide the value to the children components.

<MyContext.Provider value={/* some value */}>
  <Child />
</MyContext.Provider>

The useContext hook is useful when you want to avoid passing props through intermediate components. For example if you have a theme that you want to pass to multiple components you can use the useContext hook to avoid passing the theme prop to all the components.

const ThemeContext = React.createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
      <Footer />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div>Theme: {theme}</div>;
}

function Footer() {
  const theme = useContext(ThemeContext);
  return <div>Theme: {theme}</div>;
}

This can be used at many levels of the component tree to avoid passing props through intermediate components all the way down the tree. Any child component of Toolbar or Footer will be able to access the ThemeContext value.

useReducer

The useReducer hook is used to manage complex state logic in a component. This hook must be called from inside a component or another hook.

import { useReducer } from 'react';

const initialState = { count: 0 };

const reducer = (state, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
};

const [state, dispatch] = useReducer(reducer, initialState);

This should be used when you have complex state logic that involves multiple sub-values or when the next state depends on the previous state. For example if you have a counter that you want to increment or decrement you can use the useReducer hook like the example above.

function Counter() {
  const initialState = { count: 0 };

  const reducer = (state, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return { count: state.count + 1 };
      case 'DECREMENT':
        return { count: state.count - 1 };
      default:
        return state;
    }
  };

  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <h1>{state.count}</h1>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

The reason why you would use this instead of useState directly is when the state logic is more complex. For example if you have a form that has multiple fields and you want to update the state based on the field that is being updated you can use the useReducer hook.

function Form() {
  const initialState = { name: '', email: '' };

  const reducer = (state, action) => {
    switch (action.type) {
      case 'CHANGE_NAME':
        return { ...state, name: action.payload };
      case 'CHANGE_EMAIL':
        return { ...state, email: action.payload };
      default:
        return state;
    }
  };

  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <form>
      <input
        type="text"
        value={state.name}
        onChange={(e) => dispatch({ type: 'CHANGE_NAME', payload: e.target.value })}
      />
      <input
        type="email"
        value={state.email}
        onChange={(e) => dispatch({ type: 'CHANGE_EMAIL', payload: e.target.value })}
      />
    </form>
  );
}

useCallback

useCallback will let you cache a function so that it doesn't change between renders. This is useful when you want to pass a function to a child component that relies on the function not changing.

const memoizedCallback = useCallback(
    () => {doSomething(a, b);},
    [a, b]
);

The first parameter is the function you want to memoize and the second parameter is an array of dependencies. If any of the dependencies change the function will be re-created.

This hook should be used when you want to optimize performance by avoiding unnecessary re-renders. For example if you have a component that takes a function as a prop and you don't want it to re-render when the parent component re-renders. This is when you will use the useCallback hook.

In this example

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

The ShippingForm takes a parameter of handleSubmit but it is nested inside a div which has a theme parameter, when theme is changed React will re-render the child components and therefore the handleSubmit function will be re-created. To avoid this you can use the useCallback hook.

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback(() => {
    // ...
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

Now the handleSubmit function will only be re-created when productId or referrer change.

The useCallback hook is similar to the useMemo hook but it is used for functions instead of values. useMemo will cache the value of the function and useCallback will cache the function itself, avoiding the function to be rebuilt. Therefore you can achieve the samething by doing the following.

function useCallback(fn, deps) {
  return useMemo(() => fn, deps);
}

If you are writing custom hooks then you can use the useCallback hook to memoize the function that you are returning.

function useHandleSubmit(productId, referrer) {
  return useCallback(() => {
    // ...
  }, [productId, referrer]);
}

This will ensure that the consumer of the custom hook can optimize their own code when it's needed.

useMemo

The useMemo hook will let you cache the value of a function so that it doesn't change between renders. This is useful when you want to avoid recalculating the value of a function on every render.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

The first parameter is the function you want to memoize and the second parameter is an array of dependencies. If any of the dependencies change the function will be re-calculated.

For example if you need to run a filter on your data and you don't want to re-run the filter on every render you can use the useMemo hook.

function ProductList({ products, filter }) {
  const filteredProducts = useMemo(() => products.filter(filter), [products, filter]);

  return (
    <ul>
      {filteredProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

useTransition

The useTransition hook allows you to update the state without blocking the UI from rendering

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

The useTransition hook returns an array with two values. The first value is a boolean that indicates if the transition is pending and the second value is a function that you can use to start the transition.

This hook should be used when you want to update the state without blocking the UI from rendering. For example if you have a list of items that you want to update and you don't want the UI to block while the items are being updated you can use the useTransition hook.

function List({ items }) {
  const [isPending, startTransition] = useTransition();
  const [newItems, setNewItems] = useState([]);

  const handleClick = () => {
    startTransition(() => {
      setNewItems([...items, 'new item']);
    });
  };

  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      {isPending && <p>Loading...</p>}
      <button onClick={handleClick}>Add Item</button>
    </div>
  );
}

useDeferredValue

The useDeferredValue hook is used to defer the value of a resource like a promise. This hook must be called from inside a component or another hook.

import { useDeferredValue } from 'react';

function MessageComponent({ messagePromise }) {
  const deferredMessage = useDeferredValue(messagePromise);
  return <Message message={deferredMessage} />;
}

This can be used when you need to defer the value to help improve the performance of the application. For example if you have a message that is being fetched from an API and you don't want to show the message until it has been fetched you can use the useDeferredValue hook.

A good example of using this is inside a <Suspense> component when you want to fetch a new value, it will mean you can continue to show the old value or the fallback until the new value is ready.

function MessageComponent({ messagePromise }) {
  const deferredMessage = useDeferredValue(messagePromise);
  return (
    <Suspense fallback={<Loading />}>
      <Message message={deferredMessage} />
    </Suspense>
  );
}

useRef

The useRef hook is used to create a mutable object that persists for the lifetime of the component. This is useful when you want to store a reference to a DOM element or a value that doesn't change between renders.

import { useRef } from 'react';

const ref = useRef(initialValue);

For example if you want to store a reference to a DOM element you can use the useRef hook.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle

useImperativeHandle is a hook that allows you to customize the instance value that is exposed to parent components when using ref. This is useful when you want to expose a custom API to the parent component.

import { useImperativeHandle, forwardRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
});

This hook should be used when you want to expose a custom API to the parent component. For example if you have a custom input component that you want to expose a focus method to the parent component you can use the useImperativeHandle hook.

function App() {
  const inputRef = useRef();

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </div>
  );
}

useId

useId is a hook that generates a unique id for a component. This is useful when you want to generate a unique id for a component that can be used in other components.

import { useId } from 'react';

function MyComponent() {
  const id = useId();
  return <div id={id}>Hello World</div>;
}

useDebugValue

The useDebugValue will allow you to add a label to the React DevTools for custom hooks. This is useful when you want to provide more information about the custom hook to the developer.

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // Show a label in DevTools next to this Hook
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  }, [friendID]);

  return isOnline;
}

useSyncExternalStore

The useSyncExternalStore hook is used to sync the state of a component with an external store. This hook must be called from inside a component or another hook.

import { useSyncExternalStore } from 'react';

function MessageComponent({ messagePromise }) {
  const [message, setMessage] = useState(null);
  useSyncExternalStore(messagePromise, setMessage);
  return <Message message={message} />;
}

The external store will be something like a promise or a value that is being fetched from an API. For example if you have a message that is being fetched from an API and you want to sync the state of the component with the message you can use the useSyncExternalStore hook.

useOptimistic

The useOptimistic hook is used to update the UI optimistically before the server responds. This hook must be called from inside a component or another hook.

import { useOptimistic } from 'react';

function MessageComponent({ messagePromise }) {
  const [message, setMessage] = useState(null);
  const optimisticMessage = useOptimistic(messagePromise, setMessage);
  return <Message message={optimisticMessage} />;
}

This function is useful when you want to update the UI optimistically before the server responds. For example if you have a message that is being sent to the server and you want to show the message before the server responds you can use the useOptimistic hook.

use

The use hook used to read a value of a resource like a promise. This hook must be called from inside a component or another hook.

import { use } from 'react';

function MessageComponent({ messagePromise }) {
  const message = use(messagePromise);
  const theme = use(ThemeContext);
}

Custom Hooks

There are a few ways to reuse functionality in React.

  • Reuse markup - use a component
  • Reuse vanilla JS logic - use a function
  • Reuse React logic - use a custom hook

Custom hooks are JavaScript functions that use React hooks. They can be used to share logic between components.

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
}

You can then use the custom hook in your components.

function App() {
  const { data, loading } = useFetch('https://api.example.com/data');

  if (loading) {
    return <div>Loading...</div>;
  }

  return <div>{data}</div>;
}

Custom hooks can be used to encapsulate logic that is shared between components. They can be used to separate concerns and make your code more modular and reusable.

Further Reading