A simple (typescript) guide to React Hooks

A simple (typescript) guide to React Hooks

This is the first of a series of articles about hooks. Yes, I know, you can find plenty of posts like this out there but stay with me, we'll try to find different points of view and we'll analyze some edge cases that can make you hurt while using hooks. We won't cover everything and we'll skip the very basics: you already know that useState is there to let you handle... state, I'll skip this useless information, I promise.
I'm going to include typescript in the code because there are some subtle things to know and because soon or later you'll probably use it anyway. If you're not familiar with typescript don't worry, the code is highly readable anyway

You can find all the code of this article on a git repository soon, so that you can just travel through commits for each step.

Cover photo by Castorly Stock from Pexels

A simple useState example

So, let's build a simple state to handle a counter. We're going to show the value on the screen and two buttons to increase/decrease that number. Sounds easy? I know, but we'll see some important aspects that you may have missed. We'll also use this as a starting point for further discussions.

import React, { useState } from 'react';

interface HookTestProps {
  startAt?: number
}

const Counter: React.FC<HookTestProps> = ({startAt}) => {
  const [count, setCount] = useState(startAt);
  
  return <div>
    <p>{count}</p>
    <button onClick={() => setCount(count => count + 1)}>Add 1</button>
    <button onClick={() => setCount(count => count - 1)}>Rem 1</button>
  </div>
}

export default Counter

Analyze it: we're using a count variable that holds the state, initialized with startAt value, and setCount that is used to change that state. You're probably familiar with it already. What type is count? If you ask typescript, it'll tell you that  it is number or undefined. This is because the type is guessed by the startAt type that is, as said, number or undefined. When we define an hook like this, it can be better to also enforce the type and in our case it makes no sense to have a counter that can be undefined; we want it to be a number. A naive approach is to simply provide a fallback value

const [count, setCount] = useState(startAt || 0);

It works but it's not elegant: by mistake we can remove the fallback value, zero, and the compiler won't tell anything: we need to take advantage of the generic nature of useState. It accepts the type and the correct syntax is

const [count, setCount] = useState<number>(startAt);

Better. The typescript compiler will suddenly complain about startAt: it must be number but it can also be undefined and this is a problem. Good job typescript. Now it makes a lot of sense to provide a fallback value because is logically correct and also because we'll make the compiler happy 😀.

const Counter: React.FC<HookTestProps> = ({startAt = 0}) => {
  const [count, setCount] = useState<number>(startAt);
  
  return <div>
    <p>{count}</p>
    <button onClick={() => setCount(count => count + 1)}>Add 1</button>
    <button onClick={() => setCount(count => count - 1)}>Rem 1</button>
  </div>
}

Changing the value of count

Let's focus on the function that changes the value of count. We have a button to increase the count value by 1 and of course we need to know the current value of the counter to increase it by one. Sometimes you may be tempted to write code like this

<button onClick={() => setCount(count + 1)}>Add 1</button>

Spoiler alert: this code works. Is it correct? No.
The setCount function accepts two type of parameters: a number (or the type of your state) and a function which is called with the current value of the state. If you have to use the current value of the state to compute the new state, you must use the latter version. If you don't, try to think what happens if you write this

// count is 1
setCount(count + 1);
setCount(count + 1);

How much is the state at the end? Probably it'll be 2 and no 3 as you may expect.

So, long story short, use the other version

<button onClick={() => setCount(count => count + 1)}>Add 1</button>

so you are sure your changes will compute from the latest value of the state, not from the current one. Here below the final version of our counter and you can play with it on CodeSandbox.

import React, { useState } from 'react';

interface HookTestProps {
  startAt?: number
}

const Counter: React.FC<HookTestProps> = ({startAt = 0}) => {
  const [count, setCount] = useState<number>(startAt);
  
  return <div>
    <p>{count}</p>
    <button onClick={() => setCount(count => count + 1)}>Add 1</button>
    <button onClick={() => setCount(count => count - 1)}>Rem 1</button>
  </div>
}

export default Counter;

Edit priceless-archimedes-f2z60

You can find here part 2