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:
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;
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
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;
};
And the main hook function now looks like this:
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>
:
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>
:
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);
}
};
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]);
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;
And when going to the component code using this hook, which resides in another package, I get this lovely types from it:
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