DEV Community

Gavin Sykes
Gavin Sykes

Posted on

React Custom Hooks that can take props, but not always, what to do in TypeScript?

Hi everyone, a bit of a shorter article today, but I recently ran into the question of what to do in a React hook if I want it to be able to take props, but not have to?

Let's say we have a hook that fetches blog posts from an API, in its simplest form we have:

import { Post } from "@/contexts/PostContext";

interface usePostsProps {
  postedByUserId?: number;
}

interface usePostsReturn {
  posts: Array<Post>;
  status: 'loading' | 'error' | 'success';
}

export function usePosts(props: usePostsProps): usePostsReturn {
  // session fetching, state setters etc.
  let url = process.env.api_base_url;
  if (props.postedByUserId) {
    const params = new URLSearchParams('posted_by_user_id', props.postedByUserId);
    url = url '?' + params.toString();
  }
  useEffect(() => {
    // Fetching logic here
  },[props, session, etc.]);
  return { posts, status };
}
Enter fullscreen mode Exit fullscreen mode

Now this hook looks perfectly fine and will work fine as well, if you use it on your users/[userId] route as postsByThisUser = usePosts({ postedByUserId: userId }); then it will return that user's posts and you can display them.

Now let's say you also have a /posts route where you want to display all posts, regardless of who posted them. Well, that question mark in the props' type declaration allows us to send it an empty object as props with no issues: posts = usePosts({});.

So you're saying it works fine? Well, what's the problem then? Why are you writing this article?

Well, this approach presents 2 potential problems, one for the developer experience and one bug:

  • Do you really want to have to send an empty object as props when you know you don't really need to?
  • In components where you do want to filter down to a user ID, you won't get any TypeScript warnings if you inadvertently submit {} as props.

Okay, so let's adjust the type of our props to get round both these issues.

interface usePostsProps {
  postedByUserId?: number;
}
Enter fullscreen mode Exit fullscreen mode

Now, the difference between interface and type in TypeScript is often little more than personal preference. However, there are some situations where one is better than the other, or one of them even won't work at all. This will turn out to be a situation where interface won't work so we have to use type.

type usePostProps = { // Take note that type also needs an =, whereas interface doesn't allow one
  postedByUserId: number;
}
Enter fullscreen mode Exit fullscreen mode

We also removed the ? from postedByUserId, because if we are passing it props, then they have to contain that property, don't they? But now, I get an error message wherever I call usePosts({}) because, of course, it's missing the user ID, but I don't want to send a user ID, so how do I get round this? Could I maybe use a union type with that type and undefined?

type usePostProps = {
  postedByUserId: number;
} | undefined;
Enter fullscreen mode Exit fullscreen mode

So now I can use usePosts() with no empty object and no - oh, "Expected 1 arguments, but got 0."? So, you're saying I have to pass undefined as a prop now? That isn't what I want at all, and doesn't exactly look great either. What if I try null instead? Hmm, same problem. How do I fix this?

Enter void

void is another keyword that TypeScript uses to signify the presence of nothing. You may already be familiar with () => void as signifying a function that does something, but doesn't return anything. So what happens if we try void instead?

type usePostProps = {
  postedByUserId: number;
} | void;
Enter fullscreen mode Exit fullscreen mode

Now let's try usePosts() and...no error messages! But let's also double-check that it's fine when we do pass props with usePosts({ postedByUserId: userId }), also no error messages, great! But what if I now try with an empty object, as that's what we identified as a possible bug earlier? usePosts({}) now tells us "Argument of type '{}' is not assignable to parameter of type 'usePostsProps'.", which means that now, if we pass any props, it enforces the rule that those props must contain the postedByUserId property.

So now we have gone from a hook that, okay, works fine and gives you the data you want, to one that is properly type-safe and has much less potential to introduce the sort of bug that we could spend half a day looking for, because TypeScript will now actually tell us where the problem is.

Top comments (0)