Blog A.Wolf

Blog Posts

No results for 'undefined'Powered by Algolia

Counter example with React Hooks and Typescript

June 23rd, 2019 - 8 min read

At the moment, I'm learning Typescript (also called TS later) and I'd like to show a small example of how to use Typescript with React hooks.

If anything is not right or could be done easier / better, please let me know as I'm pretty new to it. I worked a lot with Flow.js - I think Typescript is pretty similar but also different in some details.

What I've noticed so far is that the tooling is working like a charm - Flow is really annoying on Windows (slow, server crashing, collisions of the command-line server with vs code, etc.).

With TS you're getting fast linting in VS Code and IntelliSense without complex installation.

First, I'd like to mention some good resources to learn more about Typescript & typed hooks (not complete list but should get you started):

I'm not going into every TS specific detail. I just want to explain the hook related typing. If anything is unclear please add a comment or look at the links mentioned above.

A basic understanding of hooks is required to understand the code here as I'm not describing every detail. If you need more information about Hooks please first have a look here.

What are we building?

The app that we're creating is a basic counter that will automatically increment every second.

If you need a useCounter hook in your app I would use the hook from react-use. The API of the demo is pretty similar to that from react-use.

I've picked a counter as it's not that difficult to implement and a good hook example.

Setup

I'm not going into it but you could get a basic setup with npx create-react-app counter-app --typescript or create a parcel.js app - I'm using Parcel but if you'd like to use CRA you can read about it here.

For the Parcel Typescript setup please have a look in the Parcel docs or the demo at the end of the post.

useCounter hook

The following code will be used in our app - don't be overwhelmed by it. I will go into the details after the code block.

import { useState, useEffect } from 'react';
import useInterval from './useInterval';

// return Type of useCOunter
type ReturnValueUseCounter = [
  number, // value
  {
    inc(step?: number): void;
    dec(step?: number): void;
    get(): number;
    set(count: number): void;
    reset(count?: number): void;
  }
];

interface ICounter {
  initialCount?: number;
  onReset?: () => void;
  onAfterReset?: (initialCount: number) => void;
  options?: {
    autoIncrementCounter: boolean;
  };
}

let renderCounter = 0;

const defaultOptions = {
  autoIncrementCounter: true,
};

const useCounter = ({
  initialCount = 0,
  onReset,
  onAfterReset,
  options = defaultOptions,
}: ICounter): ReturnValueUseCounter => {
  const [count, setCount] = useState(initialCount);
  const [state, setState] = useState({
    resetTriggered: false,
  });
  // Event handlers
  const inc = (step: number = 1) => {
    setCount(count + step);
  };
  const dec = (step: number = 1) => {
    setCount(count - step);
  };

  const get = () => count;

  const reset = (count: number = initialCount) => {
    setCount(count);
    setState({ ...state, resetTriggered: true });

    if (onReset) {
      onReset();
    }
  };

  // Side effects
  useEffect(() => {
    if (state.resetTriggered && onAfterReset) {
      setState({ ...state, resetTriggered: false });
      onAfterReset(initialCount);
    }
  }, [count]);

  options.autoIncrementCounter &&
    useInterval(() => setCount(count => count + 1), 1000);

  renderCounter++;

  console.info('render counter', renderCounter);
  
  return [count, { inc, dec, get, set: setCount, reset }];
};

export default useCounter;

The import useInterval from './useInterval' is a hook for creating an interval with setInterval - I'm not going into the details but you can read a very good post about it here.

Type definition

The first part is the type of the useCounter hook's return value. I'll repeat it here so I can describe it:

type ReturnValueUseCounter = [
  number, // value
  {
    inc(step?: number): void;
    dec(step?: number): void;
    get(): number;
    set(count: number): void;
    reset(count?: number): void;
  }
];

It's an array type that expects as first element a number which is the current counter value and as second an object type of functions. It would be also possible to extract the second part into a type CounterActions and use it in the return type.

Note:

The question mark in the function definitions e.g. inc(step?: number):void is called the greedy operator (or optional parameters / properties) and means that this type is optional. It's possible to test the effect e.g. call the set function without a parameter and TS will report an error for it by setting the question mark test-wise to the definition you can remove that error but take care to handle the undefined value correctly by either setting a default value or do an if-test for the value. More details about it can be found in the docs.

We could also do the typing inline with the function defintion like

const Counter = (params): [number, CounterActions] => { /* ... */ }

I'd prefer to extract the types to its definition as it's more readable. But inlining is OK if you're just having some types.

Now to the function definition of useCounter. It's using object desctructuring with the ICounter interface. With-out the destruct it would look like

const useCounter = (params: ICounter): ReturnValueUseCounter => { /* ... */ 
}

But then you would need to access the the parameters with params.intialCount. That's why we're desctructuring the object with

const useCounter = ({
  initialCount = 0,
  onReset,
  onAfterReset,
  options = defaultOptions,
}: ICounter): ReturnValueUseCounter => { /*...*/ }

The interface defintion ICounter is containing the intialCount which is optional and a number. onReset and onAfterReset are callback functions - onReset is called before the counter is reset and onAfterReset is called after reset.

I don't have a real use-case for it but I played a bit with how to trigger them. The options are for configuring the useCounter hook. There could be more options like roll-over to 0 at a value or enable count limiting - just to give you some ideas.

We could also use a parameter type for the initialCount if it would be something that's always used. Then the definition would look like:

const useCounter = (initialCount: number, options: ICounter): ReturnValueUseCounter => { /* ... */ }

Hook specific code

useEffect is triggered every time the count changes but the if-statement avoids calling the onAfterReset function. Why is this needed? There is no second argument to setState in hooks and with this, you can implement it similarly - not sure if there is a better option but it is working.

So the reset function will set the resetTriggered state to true and on the next render the side effect will be triggered - once it is occurred we're resetting the state so it's ready for the next call.

 options.autoIncrementCounter &&
    useInterval(() => setCount(count => count + 1), 1000);

useInterval hook is starting the interval if the option is enabled.

The renderCounter and the console.log is just for debugging so you can see how often a re-render will happen.

Usage

The hook code above can be used in our app like this:

const [value, { inc, dec, get, set, reset }] =      
  useCounter({
    initialCount: 10,
    onReset,
    onAfterReset,
  });

value is the current counter value and the desctructured functions are for triggering these actions like incrementing, decrementing etc.

Code & demo

You can find the demo in this Codesandbox and the souce code here.

If Codesandbox displays an error message Target container is not a DOM element open index.tsx and add an empty row so the page reloads - this fixed the problem for me. Screenshot codesandbox error

Conclusion

Now you should know how to add type definitions to your custom hooks. But I think the best to learn more about Typescript is by reading typed code and I think I'll have a look at the code from react-use - so I can learn more about hooks & Typescript.

What could be improved in the demo? I wanted to add theming with a dark & light theme maybe I'll add this in a separate demo as it would be interesting if it's possible with a custom hook like useTheme with styled-components - if it's not already available.

Adding unit tests would be also great to have - I haven't checked how to test hooks.

©2022 Alexander Wolf