TypeScript + React Hooks: Part 3

11 December 2020

In this 3rd article in our series on using React Hooks with TypeScript, we will show how to enhance 'primitive' hooks into more complex functions, and highlight some of the rules and limitations to be mindful of when creating your own hooks

In our previous article we talked about some of the incredibly useful hooks built into React. Here in part 3, we will show how to enhance these "primitive" hooks into more complex functions, and highlight some of the rules and limitations to be mindful of when creating your own hooks.

Hook Rules

Creating your own hook is as easy as creating any function. However, there are some rules that are strongly recommended by React:

  1. The hook name must start with the prefix use.
  • e.g. useUser() or useThree()
  1. A hook can only be called:
  • At the top level of a react component (i.e. not in a conditional statement).
  • From another hook (composing!).

To help enforce these rules, there is a handy eslint plugin (you are using eslint right? 😊) that will scream at you if you break them.

Creating Hooks

We are now ready to create our first hook. In the following example, the hook useDivideByTwo will take a number input (thank you TypeScript) and return the number divided by two. We adhere to the guidelines of prefixing the hook name with the use prefix and calling it at the top level.

Codepen
const MyFirstHookComponent: React.FC = () => {
  const result = useDivideByTwo(10);
  
  return (
    <div>result: {result}</div>
  );
}

function useDivideByTwo(input: number): number {
  return input / 2;
}

The following example breaks all 'the rules' - it calls a hook conditionally, it invokes a hook within a non-hook function, and it names a hook (doHookStuff) without the use prefix.

This code will compile without complaint, however, because React relies on hook-calls being in the same order on each render to manage state, any changes to this order could result in the wrong state being injected into a variable, completely breaking the component. See the wonderful official documentation for more details on why this is the case.

Basically, do not do this unless you want some unexpected behaviour that is very difficult to debug!

const iHateHooks = true;

const MyFirstHookComponent: React.FC = () => {
  let result;
  if (iHateHooks) {
    result = 5;
  } else {
    result = doHookStuff();
  }
  
  return (
    <div>result: {result}</div>
  );
}

function doHookStuff(): number {
  return useDivideByTwo(10);
}

function useDivideByTwo(input: number): number {
  return input / 2;
}

Composing hooks

Let’s correct the previous example:

Codepen
const MyFirstHookComponent: React.FC = () => {
  const result = useFive()
  
  return (
    <div>result: {result}</div>
  );
}

function useFive(): number {
  return useDivideByTwo(10);
}

function useDivideByTwo(input: number): number {
  return input / 2;
}

Much better 😌

This is our first real exposure to composing hooks. We are calling the hook useFive which then calls the hook useDivideByTwo.

We can call as many hooks as we want in these functions as long as we adhere to the guidelines:

Codepen
const MyFirstHookComponent: React.FC = () => {
  const result = useFive()
  
  return (
    <div>result: {result}</div>
  );
}

function useFive(): number {
  return useDivideByTwo(useTen());
}

function useTen(): number {
  return 10;
}

function useDivideByTwo(input: number): number {
  return useDivide(input, 2);
}

function useDivide(input: number, by: number): number {
  return input / by;
}

Now we have a reusable useDivide and useTen hook. All composed together in small reusable functions.

A more complex example

Of course, the above are very simple examples. What if we wanted to do something with DOM listeners and mouse click events?

Codepen
const MyFirstHookComponent: React.FC = () => {
  const [mouseDown, setMouseDown] = React.useState(false);
  const onMouseDown = React.useCallback(() => {
    setMouseDown(true);
  });
  const onMouseUp = React.useCallback(() => {
    setMouseDown(false);
  });
  useMouseDownListener(onMouseDown);
  useMouseUpListener(onMouseUp);
  
  return (
    <div>Mouse feelings: {mouseDown ? "😡" : "😌"}</div>
  );
}

function useMouseDownListener(onMouseDown: () => void): void {
  React.useEffect(() => {
    document.addEventListener("mousedown", onMouseDown);
    
    return () => document.removeEventListener("mousedown", onMouseDown);
  }, [onMouseDown]);
}

function useMouseUpListener(onMouseUp: () => void): void {
  React.useEffect(() => {
    document.addEventListener("mouseup", onMouseUp);
    
    return () => document.removeEventListener("mouseup", onMouseUp);
  }, [onMouseUp]);
}

Here we are adding an event listener for both mouse down and mouse up events. We use the useEffect hook to ensure this is cleaned up when the component is removed from view (otherwise we will add a gradual memory leak). Also the callbacks passed into these hooks are created via the useCallback hook to ensure we only register a callback once.

There is a lot of boilerplate going on in the component which would make any developer’s WTFPM go off the charts, so how do we clean this up and create something reusable?

Codepen
const MyFirstHookComponent: React.FC = () => {
  const {mouseDown} = useMouseListener()
  
  return (
    <div>Mouse feelings: {mouseDown ? "😡" : "😌"}</div>
  );
}

function useMouseListener(): {mouseDown: boolean; mouseUp: boolean} {
  const [mouseDown, setMouseDown] = React.useState(false);
  const onMouseDown = React.useCallback(() => {
    setMouseDown(true);
  });
  const onMouseUp = React.useCallback(() => {
    setMouseDown(false);
  });
  useMouseDownListener(onMouseDown);
  useMouseUpListener(onMouseUp);
  
  return {
    mouseDown: mouseDown,
    mouseUp: !mouseDown
  }
}

function useMouseDownListener(onMouseDown: () => void): void {
  React.useEffect(() => {
    document.addEventListener("mousedown", onMouseDown);
    
    return () => document.removeEventListener("mousedown", onMouseDown);
  }, [onMouseDown]);
}

function useMouseUpListener(onMouseUp: () => void): void {
  React.useEffect(() => {
    document.addEventListener("mouseup", onMouseUp);
    
    return () => document.removeEventListener("mouseup", onMouseUp);
  }, [onMouseUp]);
}

Now we have something a little easier to reason about, and reusable! In the component code we can do our normal flow control and conditional checks, while the hook stays in a "pure" state with no conditional logic at all keeping React happy.

Conclusion

Here at Instil we really like using hooks to allow our complex code to be easily tested, reused, and cleanly separated out. This allows us to create new React components, or just sharing some common behaviour between them.

Feel free to take the Codepens provided and try your hand at enhancing them (maybe adding a mousemove listener?) to really get to grips with how powerful composing hooks can be.

Article By
blog author

Neil Armstrong

Senior Engineer