Searching with useState

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
shared.ts
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 in useState
  • 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.