# Using React 19 useOptimistic Hook in OpenCut: A Complete Implementation Guide

> Learn to implement React 19 useOptimistic hook in OpenCut. This guide shows how to instantly update your UI with TanStack Query, improving user experience.

- Repository: [OpenCut.app/OpenCut](https://github.com/OpenCut-app/OpenCut)
- Tags: how-to-guide
- Published: 2026-06-23

---

**OpenCut does not currently ship with a built-in optimistic update hook, but you can implement one using TanStack Query's `useMutation` lifecycle methods to instantly update the UI while syncing with the backend API.**

The OpenCut video editor's frontend is a modern React application built with **TanStack Router** and a component library located in `apps/web/src/components/ui`. While the codebase handles UI rendering and routing comprehensively, optimistic data-fetching patterns must be added manually to the **API** module at [`apps/api/src/index.ts`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/api/src/index.ts). This guide shows you how to implement optimistic updates in OpenCut using the existing TanStack Query integration rather than React 19's native `useOptimistic` hook, which is better suited for local state management while TanStack Query handles server-state synchronization.

## Understanding OpenCut's Frontend Architecture

OpenCut's web application relies on **TanStack Router** for routing and server-state management. The router configuration in [`apps/web/src/router.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/router.tsx) initializes a `queryClient` that powers data fetching across the application.

The router setup creates an internal query client that you can access for optimistic updates:

```tsx
// apps/web/src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';

export function getRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0,
  });

  return router;
}

// Export the query client that TanStack Router creates internally
declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof getRouter>;
  }
}

```

The `router._router.state.queryClient` field provides access to the cache instance, though you will typically use the `useQueryClient` hook from `@tanstack/react-query` within components.

## Implementing the Optimistic Update Hook

Because OpenCut bundles `@tanstack/react-router-ssr-query` for server-side rendering support, you can leverage the full TanStack Query ecosystem to build optimistic UI patterns.

### Installing Dependencies

First, add TanStack Query to the web application if not already present. Check [`apps/web/package.json`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/package.json) for the dependency, or install it:

```bash
npm add @tanstack/react-query

# or

bun add @tanstack/react-query

```

This brings the `useMutation` and `useQueryClient` hooks into the bundle, which are essential for the optimistic update pattern.

### Creating the useOptimisticUpdate Hook

Create a new file at [`apps/web/src/hooks/useOptimisticUpdate.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/hooks/useOptimisticUpdate.tsx) that encapsulates the optimistic lifecycle. This hook wraps `useMutation` and handles the cache manipulation automatically:

```tsx
// apps/web/src/hooks/useOptimisticUpdate.tsx
import { useMutation, QueryClient, useQueryClient } from '@tanstack/react-query';
import type { MutationFunction, UseMutationOptions } from '@tanstack/react-query';

/**
 * Generic optimistic-update hook.
 *
 * @param mutationFn   Function that performs the real server request.
 * @param queryKey     Cache key of the data that will be updated optimistically.
 * @param getOptimisticUpdate  (optional) Function that returns the optimistic
 *                             value based on the variables passed to mutate().
 */
export function useOptimisticUpdate<TData, TVariables, TContext = unknown>(
  mutationFn: MutationFunction<TData, TVariables>,
  queryKey: readonly unknown[],
  getOptimisticUpdate?: (variables: TVariables) => TData,
  options?: UseMutationOptions<TData, unknown, TVariables, TContext>
) {
  const queryClient = useQueryClient();

  return useMutation<TData, unknown, TVariables, TContext>(mutationFn, {
    // Called *before* the network request – we apply the optimistic change.
    async onMutate(variables) {
      // Cancel any outgoing refetches so they don’t overwrite our optimistic data.
      await queryClient.cancelQueries({ queryKey });

      // Snapshot the previous value.
      const previousData = queryClient.getQueryData<TData>(queryKey);

      // Apply optimistic update if a mapper is supplied.
      if (getOptimisticUpdate) {
        const optimisticData = getOptimisticUpdate(variables);
        queryClient.setQueryData<TData>(queryKey, optimisticData);
      }

      // Return a context object with the previous data to enable rollback.
      return { previousData } as unknown as TContext;
    },

    // If the request fails, roll back to the previous cache.
    onError(_err, _variables, context) {
      if (context && (context as any).previousData !== undefined) {
        queryClient.setQueryData<TData>(queryKey, (context as any).previousData);
      }
    },

    // After success or failure, refetch to sync with the server.
    onSettled() {
      queryClient.invalidateQueries({ queryKey });
    },

    // Spread any extra options supplied by the caller (e.g., retry, staleTime).
    ...options,
  });
}

```

This implementation follows the **optimistic update pattern** by immediately updating the cache in `onMutate`, rolling back in `onError`, and reconciling with the server in `onSettled`.

### Wiring the Hook to UI Components

To use this hook in OpenCut's UI layer, import it into a component such as the Delete Project button at [`apps/web/src/components/ui/button.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/button.tsx):

```tsx
// apps/web/src/components/ui/button.tsx
import { FC } from 'react';
import { useOptimisticUpdate } from '#/hooks/useOptimisticUpdate';
import { fetchDeleteProject } from '#/api/project'; // Implement this fetch wrapper

type DeleteButtonProps = {
  projectId: string;
};

export const DeleteProjectButton: FC<DeleteButtonProps> = ({ projectId }) => {
  const queryClient = useQueryClient();
  
  // Optimistically remove the project from the list cached under ["projects"]
  const { mutate: deleteProject, isLoading } = useOptimisticUpdate(
    fetchDeleteProject,                // Real API call to apps/api
    ['projects'],                      // Cache key used elsewhere with useQuery
    (vars) => {
      // Remove the item from the cached array immediately.
      const cached = queryClient.getQueryData<Project[]>(['projects']) ?? [];
      return cached.filter((p) => p.id !== vars);
    }
  );

  return (
    <button
      disabled={isLoading}
      onClick={() => deleteProject(projectId)}
      className="bg-red-600 text-white px-4 py-2 rounded"
    >
      {isLoading ? 'Deleting…' : 'Delete Project'}
    </button>
  );
};

```

The `fetchDeleteProject` function should be a thin wrapper around `fetch` or `axios` that calls the OpenCut API endpoint (e.g., `DELETE /api/projects/:id`) defined in [`apps/api/src/index.ts`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/api/src/index.ts).

### Displaying Data That Benefits from Optimistic Updates

Components that display the data should use `useQuery` with the same cache key. For example, in a route component:

```tsx
// apps/web/src/routes/__root.tsx
import { useQuery } from '@tanstack/react-query';
import { fetchProjects } from '#/api/project';

export default function ProjectsPage() {
  const { data: projects, isLoading } = useQuery(['projects'], fetchProjects);

  if (isLoading) return <div>Loading…</div>;

  return (
    <ul>
      {projects?.map((p) => (
        <li key={p.id} className="flex justify-between items-center">
          {p.name}
          <DeleteProjectButton projectId={p.id} />
        </li>
      ))}
    </ul>
  );
}

```

When the user clicks **Delete**, the UI instantly removes the project from the list. If the server reports an error, the hook rolls the cache back to its previous state automatically.

## How Optimistic Updates Work in OpenCut's Architecture

The optimistic update pattern integrates with OpenCut's existing architecture through three layers:

1. **Routing & Data Layer** – TanStack Router ([`apps/web/src/router.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/router.tsx)) creates the `queryClient` that serves as the single source of truth for server state.
2. **UI Layer** – Components in `apps/web/src/components/ui` remain declarative and side-effect-free, delegating mutation logic to the custom hook.
3. **API Layer** – Network requests live in the API package ([`apps/api/src/index.ts`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/api/src/index.ts)), maintaining clean separation between presentation and data access.

This approach reuses the existing `queryClient` from TanStack Router, keeping the implementation lightweight and consistent with OpenCut's SSR-query integration.

## Testing the Implementation

OpenCut uses **Vitest** for unit testing, configured in [`apps/web/package.json`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/package.json). To verify your optimistic update hook:

```bash
npm test

```

Write tests that assert:
- The cache changes immediately after calling `mutate`
- A failed request restores the previous state via the rollback mechanism
- The `onSettled` callback invalidates and refetches the query

## Summary

- OpenCut's frontend uses **TanStack Router** with an integrated `queryClient` for server-state management.
- The repository does not include a built-in optimistic hook, requiring a custom implementation using `@tanstack/react-query`.
- Create [`apps/web/src/hooks/useOptimisticUpdate.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/hooks/useOptimisticUpdate.tsx) to encapsulate the `onMutate`, `onError`, and `onSettled` lifecycle.
- Wire the hook into UI components like [`apps/web/src/components/ui/button.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/button.tsx) for instant UI feedback.
- The implementation maintains consistency with OpenCut's monorepo structure: UI components → Router → API layer.

## Frequently Asked Questions

### What is the difference between React 19's useOptimistic and TanStack Query's optimistic updates?

**React 19's `useOptimistic` hook is designed for local state updates**, such as form inputs or UI toggles that don't require server synchronization. **TanStack Query's optimistic mutation pattern** is better suited for OpenCut because it handles server-state, network errors, and automatic cache reconciliation. The `useOptimisticUpdate` hook implemented above provides the same instant UI feedback while maintaining data integrity with the backend API at [`apps/api/src/index.ts`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/api/src/index.ts).

### Where should I place the API fetch functions in OpenCut?

Place API fetch functions like `fetchDeleteProject` in the API module at [`apps/api/src/index.ts`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/api/src/index.ts) or in a dedicated `apps/web/src/api/` directory. This maintains the monorepo's separation of concerns: UI components import from the API layer, while the optimistic hook in `apps/web/src/hooks/` manages the cache interaction.

### How do I access the queryClient outside of React components?

The TanStack Router instance exposes `router._router.state.queryClient`, though this is a private field. According to the source in [`apps/web/src/router.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/router.tsx), you should use the `useQueryClient` hook from `@tanstack/react-query` within components, or pass the client explicitly through context if needed outside the React tree.

### Can I use this pattern with OpenCut's SSR setup?

Yes. OpenCut bundles `@tanstack/react-router-ssr-query` for server-side rendering. The `useOptimisticUpdate` hook works seamlessly with SSR because it uses the same `queryClient` instance created by the router in [`apps/web/src/router.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/router.tsx). The optimistic updates only execute on the client after hydration, ensuring no mismatch between server-rendered HTML and the initial client state.