Converting Your React Hook To TypeScript

Matti Bar-Zeev - Dec 9 '22 - - Dev Community

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:

Image description

The hook I’m about to refactor resides on a hooks package 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:



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;


Enter fullscreen mode Exit fullscreen mode

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:



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



Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

And the main hook function now looks like this:



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


Enter fullscreen mode Exit fullscreen mode

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

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

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.



const setCursor = (newCursor: number) => {
       if (newCursor >= 0 && newCursor < totalPages) {
           setInternalCursor(newCursor);
       }
   };


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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.

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



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; 


Enter fullscreen mode Exit fullscreen mode

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

Image description

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 @mattibarzeev on Twitter 🍻

Photo by Josep Martins on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .