In the first post on Recoil.js I touched upon atom
, which is a piece of state that can be read and written.
Then I followed up by showing how a selector
can be used to derive state from atoms.
This is all fine. But Atle, ever since the days of XMLHttpRequest
, JavaScript has been used to fetch data from APIs. And in 2022 I expect to be able to do this async!
I hear you!
And in this post I'm going to cover how to do exactly this.
Say we have this API for retrieving blog posts:
export interface BlogPostHeader {
title: string;
id: number;
}
export interface BlogPost extends BlogPostHeader {
content: string;
}
export interface BlogApi {
getPosts: () => Promise<BlogPostHeader[]>;
getPost: (id: number) => Promise<BlogPost | undefined>;
}
Nice, eh? Well, good enough to serve our illustration purposes I guess.
But having an API isn't enough, we need to be able to show data, right?
So you guessed it, we need those blog posts in state. And, how do we do that?
Using a selector, of course!
And, the get method of a selector can of course be async:
const blogPostListState = selector<BlogPostHeader[]>({
key: "blogPostListState",
get: async ({get}) => await api.getPosts()
});
The observant reader may ask: "where do you get the api
object from?". Let's just say that for now this is a singleton, ok? Bear with me, please.
OK, we have an api, and we have this represented as a selector. How do we use it?
Using useRecoilValue
of course!
But, the selector is async?
Yeah, right, we have to wrap things in a React <Suspense>
block. Like so:
const BlogPostList = () => {
const posts = useRecoilValue(blogPostListState)
return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>
}
export const MyBlog = () => (
<>
<h1>My awesome blog</h1>
<Suspense fallback="loading"><BlogPostList /></Suspense>
</>
);
Or, you could use useRecoilValueLoadable
and get a loadable back. This leaves more control up to you, and can be handy in some situations. But that is a topic for another day. We'll stick with Suspense for now.
But we want the user to be able to select a specific blog post as well, right?
Yep! We could store the selected post id in an atom
and call getPost in another selector
. Like so:
const selectedPostState = atom<number | undefined>({
key: "selectedPostState",
default: undefined
});
const selectedPostContentState = selector<BlogPost | undefined>({
key: "selectedPostContentState",
get: async ({get}) => {
const id = get(selectedPostState);
return id ? await api.getPost(id) : undefined;
}
});
And then we just have to wire this up. With some refactoring, because our components are getting rather messy:
const BlogPostListElement = ({post}: {post: BlogPostHeader}) => {
const setSelected = useSetRecoilState(selectedPostState);
return (
<li onClick={() => setSelected(post.id) }>
{post.title}
</li>
);
}
const BlogPostList = () => {
const posts = useRecoilValue(blogPostListState)
return (
<ul>
{posts.map(post => <BlogPostListElement key={post.id} post={post} />)}
</ul>
);
}
Don't you just love the misuse of list elements? Well. I never claimed I had an iota of design-capabilities. But you get the gist here. And I'm on a tangent.
To get back on track: What's going on here? When the user clicks a blog post we set the selectedPostState
. So, what remains is to create a component to show the selected blog post (if any):
const BlogPostContent = () => {
const post = useRecoilValue(selectedPostContentState);
return post
? <>
<h2>{post.title}</h2>
<p>{post.content}</p>
</>
: null;
}
And then we have to use this component in our app, wrapped in Suspense of course:
export const MyBlog = () => (
<>
<h1>My awesome blog</h1>
<Suspense fallback="loading"><BlogPostList /></Suspense>
<Suspense fallback="loading"><BlogPostContent /></Suspense>
</>
);
And there you have it. Bob is your fathers brother and all that.
But what is going on?
Ok, we have three pieces of state
-
selectedPostState
: an atom containing the id of the selected post (if any) -
blogPostListState
: an async selector that fetches the list of blog posts. This will only be evaluated once, as it has no dependencies -
selectedPostContentState
: an async selector that fetches the selected blog post if set. This will re-evaulate each timeselectedPostState
, as it depends on it.
And each of our components interacts with the pieces of state that they need.
I find this very clean and easy to reason about, and hopefully you'll now have a better understanding of how recoil can be used to deal with async state.
The code for this is available at playcode.io, which started bitching about exceeding the free tier, so the code isn't as clean and separated as I wanted, so for the next installment I'll find another codepen. Maybe codepen? ;)
Because, yes, there will be a next installment. We have so many other topics to cover, such as
- Organization of a project
- selectorFamilies
- error handling
- testing
- initializeState
- Loadables
- crazy chaining
And more. So, check back or subscribe or whatever ;)
Top comments (0)