a sticker i found in the garbage
a sticker i found in the garbage

Image: RealToughCandy.com

Pop Quiz: What hooks do you know?

Tags: react, web dev, hooks

December 12, 2022

Recently, I spoke to a recruiter who was getting to know me. He's a super nice guy and we got along well. As we got deeper into the conversation, he asked me which hooks I'd been using in my react development. To be honest, I drew a blank, "Uh, you mean useState?"

It wasn't a big deal that I didn't know. But now, I'm left wondering what all these glorious new hooks are that I can incorporate into my product. Ideally, I'll discover some ways to reduce some clutter and toss out some existing old-school React code snippers in favor of a hook.

What are these again?

The main idea is that hooks, since v16.8, let you use state and 'other' features without writing a class. In other words, FunctionComponents get the benefits of Class components.

The motivation is that class components become unwieldy and it's hard to share logic between them. So hooks come in as a much more direct API to get to all the React goodness without having to grasp classes or even this. (*〠_〠;)

In many ways, hooks lower the bar to entry for React and let devs focus more on function crafting.

React hooks usage examples:

Let's take a look at how we use hooks in React:

useState

  
  const [tick, setTick] = useState(0);

This hook is the one I know the most. Use it to handle 'reactive' data. When data changes state, the component will rerender.

useEffect

This one lets you tap into the React lifecycle methods:


useEffect(() => {
  /* ComponentDidMount code */
}, []);

useEffect(() => {
  /* componentDidUpdate code */
}, [var1, var2]);

useEffect(() => {
  return () => {
  /* componentWillUnmount code */
  }
}, []);

Combine them all in one function:


useEffect(() => {
  /* componentDidMount code + componentDidUpdate code */

  return () => {
  /* componentWillUnmount code */
  }
}, [var1, var2]);

useContext

Want to pass data and avoid props? The example from React docs shows how we can pass the value from the ThemeContextProvider\> so that it's used by the child components.

Child components will always consume the value from the nearest parent Provider component.


  const themes = {
    light: {
      foreground: "#000000",
      background: "#eeeeee"
    },
    dark: {
      foreground: "#ffffff",
      background: "#222222"
    }
  };

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

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

  function Toolbar(props) {
    return (
      <>
        <ThemedButton />
      </>
    );
  }

  function ThemedButton() {
    const theme = useContext(ThemeContext);

    return (
      <button
        aria-label="Always set Aria Name"
        style={{ background: theme.background, color: theme.foreground }}
      >
        I am styled by theme context!
      </button>
    );
  }

useReducer

An alternative way to set state using a reducer (state, action) => newState. It returns the current state paired with a dispatch method. It's necessary when the new state depends on the old state.

The shape is like this:


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

The React docs example uses a counter to demo the functionality:


  const initialState = {count: 0};

  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return {count: state.count + 1};
      case 'decrement':
        return {count: state.count - 1};
      default:
        throw new Error();
    }
  }

  function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
      &lt;>
        Count: {state.count}
        &lt;button onClick={() => dispatch({type: 'decrement'})}>
          -
        &lt;/button>
        &lt;button onClick={() => dispatch({type: 'increment'})}>
          +
        &lt;/button>
      &lt;/>
    );
  }

useMemo

This one is useful for reducing the performance costs of expensive functions.


  const { useState, useMemo } = React;

  const CalculateFactorial = () => {
    const [num, setNum] = useState(0);

    const getFactorial = val => {
      if (val === 0) return 1;
      return val * getFactorial(val - 1);
    };

    const calculatedFactorial = useMemo(() => {
      return num ? getFactorial(num) : 0;
    }, [num]);

    return (
      <div>
        <section className="box">
          <div>Enter a number to calculate its factorial [n(n-1)]:</div>
          <input value={num} onChange={(e) =>
            setNum(e.target.value)}
          />
        </section>
        <div className="box">Factorial: {calculatedFactorial}</div>
      </div>
    );
  }

  ReactDOM.render(<CalculateFactorial />, document.getElementById('root'));

In this example, the expensive calculation will only run if the num changes, thus avoiding needless expensive rerenders.

useCallback

Similar to useMemo(), useful for reducing the performance costs of expensive functions.


  const { useState, useCallback } = React;

  const add = (fst, snd) => fst + snd;

  const AddTwoThingsComponent = () => {

    const [firstVal, setFirstVal] = useState(10);
    const incrementFirst = () => setFirstVal(num => num + 1);
    const decreaseFirst = () => setFirstVal(num => num - 1);

    const [secondVal, setSecondVal] = useState(20);
    const incrementSecond = () => setSecondVal(num => num + 1);
    const decreaseSecond = () => setSecondVal(num => num - 1);

    const additionResult = useCallback(add(firstVal, secondVal), [firstVal, secondVal]);

    return (
      <>
        <section className="box">
          <span>{firstVal}</span>
          <button aria-label="val1 +1" onClick={incrementFirst}>Click to Add 1</button>
          <button aria-label="val1 -1"onClick={decreaseFirst}>Click to Subtract 1</button>
        </section>
        <section className="box">
          <span>{secondVal}</span>
          <button aria-label="val2 +1" onClick={incrementSecond}>Click to Add 1</button>
          <button aria-label="val2 -1" onClick={decreaseSecond}>Click to Subtract 1</button>
        </section>
        <div className="box">Result: {additionResult}
        </div>

      </>
    )
  }

  ReactDOM.render(<MyComponent />, document.getElementById('root'))

useRef

So, do you want to access DOM elements? or even better, do you want to access prev state? Great! useRef lets you do that. In the below example, useRef is used to display the previously rendered value and doesn't rely on rerendering to do so.


  import React, { useRef };

  export default function App() {
    const [name, setName] = useState("");
    const preName = useRef("");

    useEffect(() => {
      prevName.current = name;
    }, [name])

    return (
      <>
        <input value={name} onChange={e => setName(e.target.value)}/>
        <div>
          Hola! Mi nombre es {name} y my nombre era {prevName}.
        </div>
      <>;
    );
  }

Other Hooks

There are still more hooks to be covered. Remaining hooks:

  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue
  • useDeferredValue
  • useTransition
  • useId
  • useSyncExternalStore
  • useInsertionEffect

However, I'll save these for a future blog post as this is a great stopping point. The hooks I cover are likely the most useful in practical applications. I've personally been aok using only these but I do want to understand the remaining opportunities so will aim to continue exploring hooks in a future post.


Loading...

© Copyright 2023 Nathan Phennel