Hooks

Hooks allow us to manage state, lifecycle methods, and other React features inside functional components.

Some common hooks: useState, useEffect, useContext, useReducer, useRef, useMemo, useCallback

Rules of Hooks

  • Only call hooks at the top level (inside the main function body)
    • i.e., don't call hooks inside loops, conditions, or nested functions
  • Only call hooks in React functions (functional components and custom hooks)
    • Functional components start with a capital letter
    • Custom hooks start with 'use', call other hooks, and return values, state, or logic (not JSX)

State Hook

[ current, setState ] = useState(initial)

  • useState : returns an array with the current state and a state setter
  • current : the current state value
  • setState(callbackFunc) : a function that we can use to update the value of the current state
    • setState( (current) => {return newState} ) : the callbackFunc always have the current state (or previous state) as a parameter and return the new state
    • Always use a callback function in setState instead of usingsetState(current + 1) because it can cause race condition
    • React will always re-render the component (unless new state is the same as old) whenever the setter function is called
      • i.e. React will calls on the functional component again and re-render the whole component, so the whole function rerun except with different state values
  • initial : the initial value of the state (optional, but recommended to put an empty object, array, or null, otherwise useState default to undefined)
import { useState } from 'react';
      
      function Counter() {
        // 1. useState call
        const [count, setCount] = useState(0);
      
        const increment = () => {
          // 2. Update state
          setCount(prev => prev + 1);
        };
      
        return (
          <div>
            // 3. Use state
            <p>Count: {count}</p>
            <button onClick={increment}>Increase</button>
          </div>
        );
      }

Effect Hook

useEffect(callbackFunc, [dependencyArray])

  • callbackFunc returns a cleanup() function to be used before re-rendering and unmounting a component
  • The 2nd argument takes a dependency array that tells useEffect to run only if any variable in the array changed, like [state]
    • Passing an empty dependency array [] tells useEffect to only run after the first render()
    • General rule of thumb is to include any variables or functions from outer scope that useEffect used, like the setter of the context

useEffect runs... everytime AFTER the component finishes rendering by calling on the callback function, or effect

  • When the component is mounted.
  • When any value in the dependency array changes, it runs the cleanup function and then the effect.
  • It runs the cleanup function one last time before unmounting a component

Uses of Effect Hook:

  • Use to fetch data from back-end, subscribe to a stream of data, manage timers and intervals, read/edit DOM
  • There are 3 key moments in a component's lifecycle that can make use of the Effect Hook
    • When component is first added/mounted to the DOM and renders
      • Only the body of the callbackFunc is run
    • When it re-render due to state or props change
      • Both the body of the callbackFunc and cleanup function is run
    • When it's removed/unmounted from the DOM
      • Only the cleanup function is run
  • Keep hooks separate based on their effect - i.e. one useEffect hook for setting up menu items and another useEffect for fetching data
import { useState, useEffect } from 'react';
      
      export default function UsersList() {
        const [users, setUsers] = useState([]);
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState(null);
      
        useEffect(() =&gt; {
          async function fetchUsers() {
            try {
              const response = await fetch("https://jsonplaceholder.typicode.com/users");
              if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
              const data = await response.json();
              setUsers(data);
            } catch (err) {
              setError(err.message);
            } finally {
              setLoading(false);
            }
          }
          fetchUsers();
        }, []);
      
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error: {error}</p>;
      
        return (
          <div>
            <h5>User List</h5>
            <ul>
              {users.map(user => (
                <li key={user.id}>
                  <strong>{user.name}</strong>{user.email}
                </li>
              ))}
            </ul>
          </div>
        );
      }

When Cleanup is Needed

  • Event listeners on elements outside the component - window, document, body
  • useEffect(() =&gt; {
            function handleResize() {
              console.log(window.innerWidth);
            }
            window.addEventListener("resize", handleResize);
          
            return () =&gt; {
              window.removeEventListener("resize", handleResize);
            };
          }, []);
  • Adding listeners using ref
  • useEffect(() =&gt; {
            const btn = buttonRef.current;
            btn.addEventListener("click", handleClick);
            return () =&gt; btn.removeEventListener("click", handleClick);
          }, []);
  • Subscribing to data, websockets, or streaming APIs
  • useEffect(() =&gt; {
            const ws = new WebSocket("wss://example.com");
            ws.onmessage = event =&gt; console.log(event.data);
          
            return () =&gt; {
              ws.close(); // Cleanup when component unmounts
            };
          }, []);
  • Timers setInterval and setTimeout must be cleaned up
  • useEffect(() =&gt; {
            const timer = setInterval(() =&gt; {
              console.log("Tick");
            }, 1000);
          
            return () =&gt; clearInterval(timer);
          }, []);
  • Initializing libraries inside useEffect (Chart.js, Leaflet, GSAP) requires cleanup
  • useEffect(() =&gt; {
            const chart = new Chart(ctx, config);
            return () =&gt; chart.destroy();
          }, []);

Reference Hook

ref = useRef(initial)

  • ref : an object with a single property: current that will persist between re-render
  • ref.current : use to access and update the value of the object
  • initial : inital value of ref.current
  • useRef will keep the reference if it's an object
  • ref objects are removed on unmount

Used to access DOM elements or persist a value between re-render, updating persisting values between render without causing re-render

  • Accessing DOM elements directly
  • Storing mutable values that don't trigger a re-render
  • Keeping previous values between re-render for comparison
// Preserving values between render
      import React, { useState, useEffect, useRef } from "react";
      
      export default function PreviousValueTracker() {
        const [count, setCount] = useState(0);
        const prevCountRef = useRef();
      
        useEffect(() =&gt; {
          prevCountRef.current = count;
        }, [count]);
      
        return (
          <div>
            <h5>Current Count: {count}</h5>
            <h3>Previous Count: {prevCountRef.current}</h3>
            <button onClick={() =&gt; setCount(c =&gt; c + 1)}>Increment</button>
            <button onClick={() =&gt; setCount(c =&gt; c - 1)}>Decrement</button>
          </div>
        );
      }

// Saving ref of JSX elements/components to be used else where
      // Passing the ref onto a sibling component or use it at parent
      function Parent() {
          const buttonRef = useRef(null);
          return (
              <>
                  <Child buttonRef={buttonRef}/>
                  <UseRef buttonRef={buttonRef}/>
              </>
          );
      }
      -------------------------------------------
      function Child({ buttonRef }) {
        return (
          <button ref={buttonRef} />
        );
      }

Context Hook

React Context is another way to pass information to components and their children without usingprops

  • Use React Context to give many components access to the same state
  • React Context also allow any component children component to get access without having to drillprops down to deeply nested children
// JSX
      // ThemeContext.js
      // 1. Create the ThemeContext and ThemeProvider
      "use client";
      import {createContext, useContext, useState} from "react";
      
      const ThemeContext = createContext();
      
      export function ThemeProvider({children}) {
        const [theme, setTheme] = useState("light");
        return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
                  {children}
          </ThemeContext.Provider>
                );
      }
      
      export function useTheme() {
         return useContext(ThemeContext);
      }
      --------------------------------------------------------------
      // app/layout.js
      import {ThemeProvider} from 'context/ThemeContext';
      
      export default function RootLayout({children}) {
          return (
            <html lang="en">
              <body id="root">
                // 2. Wrap children in the Provider
                <ThemeProvider>
                  <Child />
                </ThemeProvider>
              </body>
            </html>
          );
      }
      --------------------------------------------------------------
      / Child.js
      // Using the value
      import {useTheme} from 'context/ThemeContext';
      
      function Child(){
          const {theme, setTheme} = useTheme();
          return (
            <button
              onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Toggle Theme
            </button>
          );
      }
`

Memo & Callback Hooks

Memo Hook : optimizes expensive computations by caching results/values so it doesn't have to be recalculated on re-render

  • Can be used to memoize a component so that it doesn't re-render unless props changed

Callback Hook: prevents unnecessary function re-creations; same as memo except for saving function

  • Use cases: if the function is used as a dependency or if it's passed to memorized children

Useful for preventing the reference of an variable/function from changing if it's used in a dependency array

import { useState, memo } from "react";
      
      // Child was memorized and won't re-render unless onFetch changes
      const Child = memo(({ onFetch }) => {
        console.log("Child rendered");
        return <button onClick={onFetch}>Fetch Data</button>;
      });
      
      export default function App() {
        const [count, setCount] = useState(0);
      
        // fetchData is a function and will be re-created on every render so the Child's prop will change with every render, causing infinite re-render unless a callback hook is used to memorize the fetchData 
        const fetchData = useCallback(() => {
          console.log("Fetching data...");
        }, []); 
      
        return (
          <div>
            <h2>Count: {count}</h2>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <Child onFetch={fetchData} />
          </div>
        );
      }

Reducer Hook

Advanced State Management - An alternative to useState when state logic is complex.