Async React Demo

repository·main·Indexed 43 days ago

https://github.com/rickhanlonii/async-react

An experimental demonstration of the 'Async React' vision from React Conf 2025. It showcases native handling of async transitions and Suspense across routing, data fetching, and design components, featuring a mockable Express backend and a set of shimmer-based loading UI components.

Tokens
3.6K
Snippets
19
Records
20
Agent score
56%

What's inside Async React

  1. How Async React handles transitions and loading states

    main

    The Async React vision aims to reduce the need for manual API management by integrating async capabilities into core layers:

    • Routing: Uses transitions by default, so navigation updates do not require manual wrapping in transitions.
    • Data Fetching: Uses Suspense by default, allowing the use of transitions and Suspense throughout the application.
    • Design Components: Expose action props so that callbacks are executed within async transitions by default. These components use optimistic updates to automatically handle results and delayed loading states.

    Network Timing Behavior:

    • Fast network (<150ms): The app feels synchronous with no visible loading states.
    • Slow network (>150ms): The app automatically displays loading states and batches updates to prevent async bugs.
  2. Setup the Async React Demo project

    main

    To run the demo locally, install dependencies using yarn and start the frontend development server with yarn dev.

    To use a real backend instead of a mock, update your .env file and run the server command:

    1. Set VITE_USE_REAL_SERVER=true in .env.
    2. Run yarn server in a separate terminal.

    Using a real backend is recommended when analyzing React Performance Tracks to observe actual network requests.

    yarn
    yarn dev
    
    # For real backend
    # Update .env to VITE_USE_REAL_SERVER=true
    yarn server
  3. Implement search and tab filtering with Suspense and async actions

    main

    In the Async React model, UI components like Design.SearchInput and Design.TabList use changeAction props to handle updates via async transitions. This allows the application to react to parameter changes (like search queries or tabs) while using Suspense to manage loading states for the data being fetched.

    export default function Home() {
      const router = useRouter();
      const search = router.search.q || "";
      const tab = router.search.tab || "all";
    
      function searchAction(value) {
        router.setParams("q", value);
      }
      function tabAction(value) {
        router.setParams("tab", value);
      }
      async function completeAction(id) {
        await data.mutateToggle(id);
        router.refresh();
      }
      return (
        <>
          <Design.SearchInput value={search} changeAction={searchAction} />
          <Design.TabList activeTab={tab} changeAction={tabAction}>
            <Suspense fallback={<Design.FallbackList />}>
              <LessonList
                tab={tab}
                search={search}
                completeAction={completeAction}
              />
            </Suspense>
          </Design.TabList>
        </>
      );
    }
  4. Implement a login form using async actions

    main

    The Design.Button component accepts an action prop. When provided, the callback is treated as an async transition. This allows for simple, declarative code that handles navigation and prefetching automatically.

    export default function Login() {
      const router = useRouter();
      const [fields, setFields] = useState(initialFieldData);
    
      async function submitAction() {
        await login(fields.username, fields.password);
        await prefetchLessons();
        router.navigate("/");
      }
      return (
        <Design.LoginForm fields={fields} setFields={setFields}>
          <Design.Button action={submitAction}>Login</Design.Button>
        </Design.LoginForm>
      );
    }
  5. Prefetch lessons with prefetchLessons()

    main

    Initiates a fetch for all lessons in the background and stores the promise in the cache. This is useful for warming up the cache before a user navigates to a view. The function returns a promise that races the fetch against a 1000ms delay.

    import { prefetchLessons } from './data/index.js';
    
    // Start prefetching lessons in the background
    await prefetchLessons();
  6. Use the Express backend API for Async React Demo

    main

    The backend server provides several endpoints to simulate real-world asynchronous operations, including support for an optional delay query parameter to simulate network latency. All endpoints are configured with CORS to allow requests from any origin.

    # Example API interactions
    
    # Fetch lessons with optional tab and search query
    GET /lessons?tab=math&q=algebra&delay=1000
    
    # Toggle a specific lesson status
    POST /lesson/:id/toggle?delay=500
    
    # Perform login
    POST /login?delay=200
  7. Mutate lesson status with mutateToggle()

    main

    Sends a POST request to toggle the status of a specific lesson by its id. Upon a successful mutation, it calls revalidate() to clear the lessons cache, ensuring subsequent fetches retrieve fresh data.

    import { mutateToggle } from './data/index.js';
    
    // Toggle a lesson by ID
    await mutateToggle(123);
  8. Fetch lessons with getLessons()

    main

    Retrieve lessons based on a specific tab and search query. This function uses an internal cache to support suspense-enabled data fetching. If the requested combination of tab and search has already been requested, it returns the existing promise from the cache.

    import { getLessons } from './data/index.js';
    
    // Fetches all lessons
    const lessons = await getLessons('all', '');
    
    // Fetches lessons for a specific tab and search term
    const filteredLessons = await getLessons('javascript', 'react');
  9. Clear the lessons cache with revalidate()

    main

    Resets the lessonsCache by creating a new empty Map. This is used to invalidate cached data, typically after a mutation (like mutateToggle or login) to ensure the UI fetches the most up-to-date information.

    import { revalidate } from './data/index.js';
    
    // Clear all cached lesson promises
    revalidate();
  10. Merge Tailwind CSS classes with cn()

    main

    The cn utility function is used to conditionally merge CSS class names and resolve Tailwind CSS class conflicts. It combines clsx for conditional logic and tailwind-merge to ensure that the last class provided wins in case of conflicts (e.g., merging px-2 and px-4 results in px-4).

    import { cn } from "@/lib/utils";
    
    // Example usage with conditional classes and Tailwind conflict resolution
    const className = cn("text-red-500", isError && "bg-red-100", "p-4 px-2");
    // If isError is true, it merges classes and resolves Tailwind conflicts
  11. Fetch lessons via GET /lessons

    main

    Retrieves a list of lessons based on filtering criteria. This endpoint is used to populate lesson lists in the UI.

    // GET /lessons
    // Query Parameters:
    // - tab: (string) Filter by lesson category/tab
    // - q: (string) Search query string
    // - delay: (number) Artificial delay in milliseconds