Converting Your React Hook To TypeScript
Tags: react, typescript, tutorial, webdev
Published: 2022-12-09 09:18:23

In this short post I’m going to continue my TypeScript journey and convert yet another key aspect of React - the Hook. I don’t think it should be that big of a deal, but needs to be done in order to see if there are any pitfalls or new things to learn about React and TS.

I will convert the React hook called “use-pagination-hook” which is used for a Pagination component, and encapsulates the component’s logic - setting a cursor, going next and prev and invoking the `onChange` callback when needed.

For reference, below is an image of the Pagination component, and you can find it’s code [here](https://github.com/mbarzeev/pedalboard/tree/master/packages/components/src/Pagination):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxadzqfjxm53wo3u4ypw.png)

The hook I’m about to refactor resides on a [hooks package](https://github.com/mbarzeev/pedalboard/tree/master/packages/hooks) which is a “Hybrid” package, meaning that it supports both JS and TS, so introducing TypeScript code to it should be smooth.

Before we begin, let’s check the original code we’re starting from:

```javascript
import {useEffect, useRef, useState} from 'react';
 
export const NO_TOTAL_PAGES_ERROR = 'The UsePagination hook must receive a totalPages argument for it to work';
 
const usePagination = ({totalPages, initialCursor, onChange}) => {
   if (!totalPages) {
       throw new Error(NO_TOTAL_PAGES_ERROR);
   }
 
   const [cursor, setInternalCursor] = useState(initialCursor || 0);
 
   const setCursor = (newCursor) => {
       if (newCursor >= 0 && newCursor < totalPages) {
           setInternalCursor(newCursor);
       }
   };
 
   const goNext = () => {
       const nextCursor = cursor + 1;
       setCursor(nextCursor);
   };
 
   const goPrev = () => {
       const prevCursor = cursor - 1;
       setCursor(prevCursor);
   };
 
   const isHookInitializing = useRef(true);
 
   useEffect(() => {
       if (isHookInitializing.current) {
           isHookInitializing.current = false;
       } else {
           onChange?.(cursor);
       }
   }, [cursor]);
 
   return {totalPages, cursor, goNext, goPrev, setCursor};
};
 
export default usePagination;
```

So now that we know where we start from, let’s put the goggles on and get our hands dirty :)

***

We start with renaming our main hook file extension from `js` to `ts`. This should start the type check fireworks going. Right off the bat we get this error:

```bash
src/use-pagination-hook/index.ts:12:25 - error TS7031: Binding element 'totalPages' implicitly has an 'any' type.

12 const usePagination = ({totalPages, initialCursor, onChange}) => {
                           ~~~~~~~~~~

src/use-pagination-hook/index.ts:12:37 - error TS7031: Binding element 'initialCursor' implicitly has an 'any' type.

12 const usePagination = ({totalPages, initialCursor, onChange}) => {
                                       ~~~~~~~~~~~~~

src/use-pagination-hook/index.ts:12:52 - error TS7031: Binding element 'onChange' implicitly has an 'any' type.

12 const usePagination = ({totalPages, initialCursor, onChange}) => {
                                                      ~~~~~~~~

src/use-pagination-hook/index.ts:19:24 - error TS7006: Parameter 'newCursor' implicitly has an 'any' type.

19     const setCursor = (newCursor) => {
                          ~~~~~~~~~


Found 4 errors in the same file, starting at: src/use-pagination-hook/index.ts:12

```

Let’s set some types going. We know that `totalPages` and `initialCursor` are numbers. The `onChange` is a function that should receive a number arg. We also know that the only arg which is not optional is the totalPages. The UsePaginationProps type for it looks like this:

```javasciprt
type UsePaginationProps = {
   totalPages: number;
   initialCursor?: number;
   onChange?: (value: number) => void;
};
```

And the main hook function now looks like this:

```javascript
const usePagination = ({totalPages, initialCursor = 0, onChange}: UsePaginationProps) => {
... 
}
```

I also took the opportunity to set a default for the `initialCursor`. 

BTW, You might have noticed that in the original code I’m checking if the function got the `totalPages` and throw an error if it didn’t. This is one of the things TS is meant to help you with - not needing to worry about misuse of the function, but I still like the solution of throwing an error, since TS is not there on runtime and you can never know what args you’ll get. 

This last modification solves 3 of the TS issues above, but we still have the last one with `setCursor`. Before we jump right to it let’s focus on the `useState` for a sec. Since we typed the `initialCursor` as number, TS knows to infer the type for the `useState` as `useState<number>`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ywayh2rom9bv276akafr.png)

On the same note, the React ref I’m using is also automatically inferred with the type of the argument passed to it `useRef<boolean>`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1vpyf25utguwac0g2nvt.png)

What can I say, that’s nice of you TS :)

Now it is time to attend the `setCursor` function. Yeah, it should not get a value of `any` type, so let’s change it to `number`. 

```javascript
const setCursor = (newCursor: number) => {
       if (newCursor >= 0 && newCursor < totalPages) {
           setInternalCursor(newCursor);
       }
   };
```
That settles the last issue we have.
It is important to note that our `onChange` function is optional in the UsePaginationProps type and therefore can be `undefined`, lucky for us we can use the optional chaining to declare that we know such scenario exists:

```javascript
useEffect(() => {
       if (isHookInitializing.current) {
           isHookInitializing.current = false;
       } else {
           onChange?.(cursor);
       }
   }, [cursor]);
```

I’m running the build command, which in my case runs the TSC with 2 configurations, ending with 2 different artifacts, but that’s [another story](https://dev.to/mbarzeev/hybrid-npm-package-through-typescript-compiler-tsc-150c).

Here is the types declaration file generated where you can see the result of our work here:

```javascript
declare type UsePaginationProps = {
   totalPages: number;
   initialCursor?: number;
   onChange?: (value: number) => void;
};
export declare const NO_TOTAL_PAGES_ERROR = "The UsePagination hook must receive a totalPages argument for it to work";
declare const usePagination: ({ totalPages, initialCursor, onChange }: UsePaginationProps) => {
   totalPages: number;
   cursor: number;
   goNext: () => void;
   goPrev: () => void;
   setCursor: (newCursor: number) => void;
};
export default usePagination; 
```

And when going to the component code using this hook, which resides in another package, I get this lovely types from it:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jpiuqdciclgstc28u87e.png)

Short and to the point, our hook is TypeScript-ed :)

As always, if you have any questions or comments, make sure to leave them in the comments section below so that we can all learn from them.

*Hey! for more content like the one you've just read check out <a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter* :beers:

<small><small><small>Photo by <a href="https://unsplash.com/@josepmartins?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Josep Martins</a> on <a href="https://unsplash.com/s/photos/abstract-colors?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></small></small>