In this module we implement a searching posts form:
- When the page loads make a request to fetch a default list of posts
- Allow the user to search for a post by typing in the input
- When the user clicks on the search button, make a request to fetch the list of posts matching the search query
This specific use case requires to make a request when the component loads ("on mount"). This requires to use the useEffect
hook.
We are going to learn how useEffect
is not necessary with XState, and how to do without it.
We intentionally won't handle all the states as before (error, loading, etc.).
In each module we instead focus on some new pattern, in this case a request "on mount".
Shared code
Same to the other modules, also here we have some shared code that we can extract into a separate file:
Context
and initial context value- Search post fetch request
export type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export type Context = { query: string; posts: Post[] };
export const initialContext = { query: "", posts: [] };
export const searchRequest = async (query: string): Promise<Post[]> =>
fetch(`https://jsonplaceholder.typicode.com/posts?title_like=${query}`).then(
(response) => response.json()
);
The implementation uses the jsonplaceholder API.
Searching with useState and useEffect
The principles to implement the searching logic are the same as the previous module:
- Store
Context
inuseState
- Update
Context
when the user types in the input - Add function to perform async request to fetch posts
- Update posts when the request returns a valid response
import { useState } from "react";
import { initialContext, searchRequest, type Context } from "./shared";
export default function UseState() {
const [context, setContext] = useState<Context>(initialContext);
const submitSearch = async () => {
const newPosts = await searchRequest(context.query);
setContext({ ...context, posts: newPosts });
};
return (
<div>
<div>
<input
type="search"
value={context.query}
onChange={(e) => setContext({ ...context, query: e.target.value })}
/>
<button type="button" onClick={submitSearch}>
Search
</button>
</div>
{context.posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
))}
</div>
);
}
The key change is the initial request to fetch the default list of posts.
Performing a request when the component loads is a common pattern in react. We achieve this by using the useEffect
hook with an empty dependency array:
import { useEffect, useState } from "react";
import { initialContext, searchRequest, type Context } from "./shared";
export default function UseState() {
const [context, setContext] = useState<Context>(initialContext);
const submitSearch = async () => {
const newPosts = await searchRequest(context.query);
setContext({ ...context, posts: newPosts });
};
useEffect(() => {
submitSearch();
}, []);
return (
<div>
<div>
<input
type="search"
value={context.query}
onChange={(e) => setContext({ ...context, query: e.target.value })}
/>
<button type="button" onClick={submitSearch}>
Search
</button>
</div>
{context.posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
))}
</div>
);
}
useEffect
is hard to use correctly. We need to account for race conditions, request dependencies, cleanup, and more.
That's why it's generally recommended to use libraries like SWR or TanStack Query for server requests like this.