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 };
}
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;
}
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;
}
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;
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;
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)