React Custom Hooks that can take props, but not always, what to do in TypeScript?
Tags: react, customhook, hook, typescript
Published: 2023-10-20 10:34:54

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:

```typescript
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 };
}
```

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.

```typescript
interface usePostsProps {
  postedByUserId?: number;
}
```

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`.

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

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?

```typescript
type usePostProps = {
  postedByUserId: number;
} | undefined;
```

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?

```typescript
type usePostProps = {
  postedByUserId: number;
} | void;
```

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.