Search

Intro to React Hooks

React Hooks enable developers to add state, lifecycle events, and cleanup to functional components.  This article introduces the basics of writing React functional components with Hooks, and we will even see how you can build your own Hooks providing reusable component logic across your app!

Why Hooks?

React functional components are just functions that are called wherever you put their JSX tag; no constructor, no lifecycle functions, and no class members.  Since functional components don't have a class to hold their state and respond to lifecycle events, Hooks are used to add state and effects to these components that React understands, replacing methods like setState and componentDidMount.  You can use functional components with Hooks alongside your existing class components in React, even using both as needed!

All of the lifecycle for setting up state and performing effects is moved into functions called Hooks that should be called towards the beginning of your function.  Even Hooks are just functions, so you can easily make your own Hooks that provide components with reusable functionality!  This is really where Hooks shine compared to class components.  Hooks make it easy to share composable features across components that need similar plumbing, without having to wrap the component in a higher-order component.

The basic Hooks

useState is the most essential hook, allowing you to store persistent state for a component.  It returns an array/tuple of two values: the first is the state variable itself, and the second is a function you call whenever you want to update the state.  Any time you update state, the component will re-render.

import { useState } from 'react';

// This is a complete React functional component!
const CounterComponent = (props) => {
  const [counter, setCounter] = useState(0);
  
  const incrementCounter = () => setCounter(counter + 1);
  
  return (
    <div>
      {counter}
      <button onClick={incrementCounter}>Count</button>
     </div>
   );
}

useEffect allows you to run code each time your component renders.  It may also be supplied an array of dependencies, in which case the code will only run when any of the dependencies change.  

// runs after each render
useEffect(() => {
  console.log("render");
});

// runs whenever counter changes
useEffect(() => { 
  window.alert(`Counter is now: ${counter}`);
}, [counter]);

If you want it to only run once when the component mounts, you can pass an empty dependency array.  

// runs once when component mounts
useEffect(async () => {
  const profile = await fetch('/profile');
  console.log(profile.name);
}, []);		

You may optionally provide a cleanup function to be run before your component unmounts.  This is helpful if you need to add event listeners (React props like onClick are typically preferred) or set global callbacks that need to be removed when your component unmounts.

// runs once when the component mounts
useEffect(() => {
  const onClick = () => window.alert('Click!');

  document.addEventListener('click', onClick);
    
  // cleanup function runs before unmount
  return () => {
    document.removeEventListener('click', onClick);
  }
}, []);

useMemo allows you to compute a memo-ized value when a list of dependencies change.  It is similar to useEffect except the return value is the return value of its callback function, and it runs during the component render, not after.

const counterDisplay = useMemo(() => {
  if (counter === 0) {
    return "Click 'count' to start counting!";
  } else {
    return counter;
  }
}, [counter]);

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

memo is also commonly used to optimize components, preventing code from being re-run unless the component's props change.  This provides the same functionality as React.PureComponent.  Any component wrapped in React.memo() will keep its previous returned render until its props change!

// Any instances of CounterComponent will now only update when its props change!
export default React.memo(CounterComponent);

Using refs

useRef creates a reference containing a value that persists for the lifetime of the component.  It is like useState, except you can update it without triggering a re-render of the component.  

const myRef = useRef('hello'):
console.log(myRef.current);
// hello
myRef.current = 'world';
console.log(myRef.current);
// world

While refs are useful just for storing values across renders, Refs can also be attached to components to get access to the underlying DOM elements from React components.

const input = useRef(null);

const focusInput () => {
  // element refs give us access to the underlying DOM element
  // so we can call functions like this imperatively!
  input.current.focus();
}

return (
  <div>
    <input type="text" ref={input} />
    <button onClick={focusInput}>Focus input</button>
  </div>
);

If you want to run a function when a ref is attached, use useCallback.

const measuredRef = useCallback((node) => {
  const headerHeight = node.getBoundingClientRect().height;
  window.alert(`The height of this header is: ${headerHeight}`);
}
return <h1 ref={measuredRef}>Hello, world</h1>;

It is important that Hooks are called in a specific order each time the component runs (they should never be inside if statements), because that's how React actually knows which Hook is which.  React doesn't know your variable names, it's just based on the order the Hooks are called.  Much like with JSX, this is a great example of how a library is able to take the existing features of a language and creatively adapt it to support new development paradigms.

Building your own Hooks

If you find yourself writing lots of components that share similar code, you can move that logic out into its own custom hook!  Hooks really are just functions that are React aware, meaning Hooks can use Hooks of their own to manage state and lifecycle.  Any logic that exists in your functional component code can be moved into its own Hook.  

const useCounter = () => {
  const [counter, setCounter] = useState(0);
  
  const incrementCounter = () => setCounter(counter + 1);
 
  const counterDisplay = useMemo(() => {
    if (counter === 0) {
      return "Click 'count' to start counting!";
    } else {
      return counter;
    }
  }, [counter]);
  
  return { counter, incrementCounter, counterDisplay };
}

const CounterComponent = () => {
  const { counterDisplay, incrementCounter } = useCounter();
  
  return (
    <div>
      {counterDisplay}
      <button onClick={incrementCounter}>Count</button>
     </div>
   );
}

Each time your component renders, a new instance of your Hook is created, and whenever your component unmounts, cleanup functions in useEffect()s within your Hook will be run.  Consumers of your hook all receive separate instances, so any shared or expensive state (like for database connections) should be moved into a singleton that you only create and initialize if it doesn't already exist.

An important and useful note is that any state changes inside of the Hook will trigger a re-render in any components that use it!  Hooks allow for full encapsulation of React logic into reusable functions.

Infernal Ocean
A holistic approach to skin health