Hooks & TypeScript

11 minutes to read

Hooks are now a part of React starting in version 16.8. And I've been rather mehhh about them, then I started using them and got a little more aight about them, and finally I started using Hooks with TypeScript and was like well alright.

Doctor Strange putting on his cape with confidence
The Hooks API is like your Cloak of Levitation, gives you powers, saves your sanctum being attacked by Kaecilius (maybe I shouldn't write blog posts and watch Marvel movies at the same time, tweet me if I'm wrong).

Generics in TypeScript

TypeScript has this concept of a "generic". When you're writing a function you may not know what parameter you're getting, what interface it matches, or what type it is, so as we add type annotations, we can add this "generic" type and allow the consumer to be able to provide their own type for the parameter.

function identity<T>(arg: T): T {
  return arg;
}

In this very contrived example, we add <T> before the function parentheses and then we have T as a type to use. Now we can declare, our parameter has a type of T. This will let us either let TypeScript infer the type and know or let us explicitly define what type our function should work with.

const id = identity<string>("user");

Or with a class:

class LocalCache<T> {
  public items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  remove(item: T): void {
    const key: number = this.items.find(item);
    if (key) {
      this.items.splice(key, 0);
    }
  }
}

const cache = new LocalCache<string>();
cache.add("Hello");
cache.remove("Hello");

We don't need to know what T represents here. We just need to know it could be anything.

The best example of where generics can get powerful is arrays methods. Arrays can have anything in them. TypeScript can guess what the result of [].map() is going to be or we can tell it. That way when we're wrong, our compiler tells us.

[1, 2, 3, 4, 5, 6].map<string>(n => n.toString());

Then there's Array.reduce() where you take an array, and break it down to a single element.

const sum = [1, 2, 3, 4, 5, 6].reduce<number>((acc, val) => acc + val, 0);

This is a basic example. But .reduce() is great for iterating through a list to build any value you want. But you can declare a generic to both methods I described and know when you break the contract of that type.

For more about .reduce() check this post out from Sarah Drasner, The Almighty Reducer

Generics give us a way to be more explicit with our code without making assumptions. You can read more about Generics in TypeScript in their docs.

My point being, generics make hooks really strongly typed and can prevent you from making mistakes as you work and comeback to your work.

Benedict Cumberbatch as Doctor Strange waving his hands in practice and making a weird face
TFW all this handwaving makes sense

React Hooks with TypeScript

For the record, I gained all of what follows from reading through React's type definitions. The value of adding these type annotations to your code is that we won't be assuming a type of any and with confidence define the interface for our state and context in the same way we're used to with React.Component.

useState()

This hook allows you to work with simple state. Normally you'd call it like this:

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </div>
  );
}

If you want to add types, you can pass an explicit generic to useState() versus an inferred one. Using this is option about but it tells the next engineer your intention explicitly.

const [count, setCount] = React.useState<number>(0);

This will tell us that count is a number, and that setCount() should expect only a number.

useContext()

useContext() allows you the ability to subscribe to a context provider from React.creatContext() and receive updates from it, like passing state down without having to explicitly pass props down to a component. This hook makes working with the Context API more explicit IMO than using a <Context.Consumer /> component.

const DarkModeContext = React.createContext({});

function App() {
  return (
    <DarkModeContext.Provider value={{ darkMode: false }}>
      <SomeComponent />
    </DarkModeContext.Provider>
  );
}

function SomeComponent() {
  const { darkMode } = React.useContext(DarkModeContext);

  return (
    <div style={{ background: darkMode ? "black" : "white" }}>
      Hello Darkness My Old Friend
    </div>
  );
}

Working in TypeScript will let us give createContext and useContext a generic to define the interface for our context. When we call context now, TypeScript knows with confidence that it has a key called darkMode and that it's not an implicit any and it in fact is a boolean.

interface IDarkModeContext {
  darkMode: boolean;
}

const DarkMode: React.Context<IDarkModeContext> = React.createContext<IDarkModeContext>({
  darkMode: false
});

function App() {
  return (
    <DarkModeContext.Provider value={{ darkMode: false }}>
      <SomeComponent />
    </DarkModeContext.Provider>
  );
}

function SomeComponent() {
  const { darkMode } = React.useContext<IDarkModeContext>(DarkModeContext);

  return (
    <div style={{ background: darkMode ? "black" : "white" }}>
      Hello Darkness My Old Friend
    </div>
  );
}

You can see this example on CodeSandbox.

useEffect() & useLayoutEffect()

Neither useEffect() or useLayoutEffect() take a generic because they don't return a value to use. But it does take two parameters, EffectCallBack and the DependencyList.

type EffectCallback = () => void | (() => void | undefined);
type DependencyList = ReadonlyArray<any>;

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

You'll see this dependency list a lot when you're working with hooks. The idea is that, when one of those dependencies change, it re-runs the first parameter you passed the hook.

useMemo()

useMemo() caches the results of expensive functions and returns the value. It takes 2 parameters: the expensive function to call and dependencies to track. When those dependencies change, it fires the expensive function again.

const value = React.useMemo(() => getExpensiveValue(a, b), [a, b]);

What is Memoization?

Let's unpack what memoization is. Memoization is a technique that allows us to wrap a function to cache the results, so that if the same input gets called to that function again, the function doesn't rerun, it returns the results from the cache.

interface IMemoizeCache {
  [key: string]: any;
}

// https://medium.freecodecamp.org/understanding-memoize-in-javascript-51d07d19430e
export function memoize<T>(fn: Function): Function {
  let cache: IMemoizeCache = {};

  return (...args: any[]): T => {
    let n = args[0];
    if (n in cache) {
      return cache[n];
    } else {
      let result = fn(n);
      cache[n] = result;
      return result;
    }
  };
}

Now let's implement a memoized function:

const addTen = (n: number): number => n + 10;

const memoizedAdd = memoize(addTen);

memoizeAdd(3); // 13
memoizeAdd(12); // 22
memoizeAdd(3); // 13

At this point the addTen() has only run twice even though we called the wrapped memoize function three times because it got only got two different inputs.

Side note: typically when you need to memoize a function, it's doing something intensive and computationally expensive, not just adding ten to a number.

What about the hook version of this?

The way of this particular hook goes, this hook memoizes the value that gets returned from the first parameter.

function getExpensiveValue(a: number, b: number): number {}

const value: number = React.useMemo(() => getExpensiveValue(a, b), [a, b]);

In this case, TypeScript would infer that useMemo() returns a number. But you can all so give it generic:

const value = React.useMemo<number>(() => getExpensiveValue(a, b), [a, b]);

The second parameter of useMemo() is a DependencyList like useEffect() and like that hook those dependencies cause the first parameter to run again.

useReducer()

useReducer() is my favorite hook. This is what people refer to as the Redux killer and honestly, it might be, but by itself is awesome regardless of any crimes it may or may not be acquitted of.

The idea behind state reducers is that you have some state and they only way that state gets updated is through dispatching an "action" object that describes the way that state should update. You have function that's your reducer, that takes in your previous state and returns your new state based on that description.

Let's take a look at a basic example:

const initialState = { count: 0 };

// function
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({ initialState }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

Now in this example, we've created a state object with a count and described, only two ways that state can update, dispatching { type: "increment" } or { type: "decrement" } to describe how our state should change. So let's add some type annotations.

interface IState {
  count: number;
}

interface IAction {
  type: string;
}

const initialState = { count: 0 };

function reducer(state: IState, action: IAction): IState {
  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] = React.useReducer<React.Reducer<IState, IAction>>(
    reducer,
    initialState
  );
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

You can see that example here.

The generic we pass useReducer() is React.Reducer<S, A> and we pass two more generics into that Reducer<> interface, S for the state of our reducer and A for the action object we pass into dispatch().

But we can also pass more values to dispatch() than just { type: "" } for our action object. Whatever object you hand dispatch() will come through as the action parameter in our reducer function.

Let's look at this more complex example here. First thing, I want to draw your attention to is how we're defining our action type.

type Action =
  | {
      type: Actions.LOADING;
    }
  | {
      type: Actions.DATA_LOADED;
      payload: string[];
    }
  | {
      type: Actions.ERROR;
      payload: string;
    };

We're using a union type, to declare our action object can only match one of those objects at a time. When we fire dispatch we at no time, without error do this:

// 🚫
dispatch({ type: Actions.LOADING, payload: ["", "", ""] });

// 🚫
dispatch({ type: Actions.DATA_LOADED });

This may seem like overkill, but it honestly doesn't take much to when your use cases are small and when they get more and more complex you'll be glad you chose that implementation to begin with (or at least that's been true for me, YMMV).

useRef()

In classes, you can use getters and setters to store and update values. With React, this was a great way to preserve values between renders without updating state. Typically developers would use refs to hold on DOM elements between renders (but you can use refs for other things).

Let's make this clear, refs don't necessarily equal DOM elements: refs represent any value you want to preserve between renders. This could literally be anything, a cache of server responses, form values, (sure) DOM elements, anything you want to remain consistent between renders.

const ref = React.useRef({});

// ref.current = {}

When you call useRef(), it returns a MutableRefObject which is an object that holds the current reference of whatever is being preserved.

interface MutableRefObject<T> {
  current: T;
}

This hook takes a generic to represent whatever you're preserving.

const count: React.MutableRefObject<number> = React.useRef<number>(0);

Since this is a mutable ref object, to update the ref all we need to do is assign a new value:

ref.count = 3;
ref.count++;

useCallback()

useCallback() is similar to useMemo(), except instead of a value, it returns a memoized callback function. It takes a callback to be recreated based on the second parameter the DependencyList.

React.useCallback(() => {}, []);

It does take a generic, the type of the function in takes:

type CountCallback = (args: number) => void;

function useMemoizedCount(): CountCallback {
  const [count, setCount] = React.useState(0);
  const memoizedSetCount = React.useCallback<CountCallback>(n => setCount(n), [count]);

  return memoizedSetCount;
}

Now we can know that the function returned takes a number and returns void.

useImperativeHandle()

If I ever run into a use case for useImperativeHandle(), I'll make a new post about it.

Benedict Cumberbatch as Doctor Strange opening some doors and making a confused face
Me trying to figure out this particular hook

useDebugValue()

useDebugValue() is meant to display a value in React's DevTools and it is only for custom hooks. This hook doesn't return anything, it just returns void. Ideally this is a string but it's generic so you can pass any value to it.

React.useDebugValue<number>(3);

React.useDebugValue<string>(isOnline ? "Online" : "Offline");

The number 3, would print as a label next to the custom hook in the DevTools. Let's say you need to transform the value we pass as the first parameter, we get it in the callback as the second parameter.

React.useDebugValue(date, date => date.toDateString());

React.useDebugValue<Date>(date, date => date.toDateString());

The second parameter is optional and it's (value: T) => any with T representing the value passed in as the first parameter. In this example it's (date: Date) => any.


Doctor Strange all powered up
See? Hooks can make you powerful.

That's all I got, enjoy using Hooks with TypeScript! πŸ πŸŽ£πŸ€―πŸ’ΈπŸ‘‹