useServerAction

The useServerAction hook simplifies server action management in Next.js client components. It uses SWR for efficient data fetching, offers conditional execution, and provides easy access to loading states, errors, and data. This TypeScript-friendly hook streamlines the process of integrating server actions into React components with minimal boilerplate.

Usage

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 "use client"; import { useEffect, useState } from "react"; import useServerAction from "@/hooks/use-server-action"; export default function Example({ name }: { name: string }) { const [shouldFetch, setShouldFetch] = useState(false); const { data, error, isLoading, mutate } = useServerAction( async () => { return `Hello ${name}!`; }, [name], { shouldFetch } ); // Refresh the data every second useEffect(() => { const interval = setInterval(() => { mutate(); }, 1000); return () => clearInterval(interval); }, [mutate]); // Conditionally control whether to start fetching the data useEffect(() => { const timer = setTimeout(() => { setShouldFetch(true); }, 5000); return () => clearTimeout(timer); }, []); if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } return <div>{data}</div>; }

Installation

npx shadcn add "https://registry.niels.foo/use-server-action.json"

With Persistence

By default, useServerAction operates purely in memory, and the cache is cleared when the tab is closed. To enable persistence across sessions, set the persist option to true.

When persistence is enabled, it modifies the behavior of SWR's isLoading state:

  1. Initially, isLoading is set to true.
  2. If cached data is available, isLoading is immediately set to false.
  3. During the actual fetch, isLoading remains false. You should look at isValidating instead anyway.

This differs from SWR's default behavior, where isLoading only becomes false after the fetch is completed, regardless of cached data availability.

1 2 3 4 5 6 7 const { data, isLoading } = useServerAction( async () => { return `Hello ${name}!`; }, [name], { persist: true } );

Source

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 "use client"; import { DependencyList, useEffect, useState } from "react"; import useSWR, { SWRConfiguration, useSWRConfig } from "swr"; type Action<T> = () => Promise<T>; interface UseServerActionSWROptions extends SWRConfiguration { /** * Conditionally control when to execute the server action. */ shouldFetch?: boolean; /** * Conditionally control whether to continue or stop refreshing the server action. */ shouldRefresh?: boolean; /** * By default `useSWR` is purely in memory and caching is cleared * when closing the tab. Use `localStorage` to persist data across sessions. */ persist?: boolean; } function generateKey<T>(action: Action<T>, deps: DependencyList = []) { return `${action.name || action.toString()}?${deps.join("&")}`; } /** * A hook for managing server actions in Next.js client components using SWR. * * @see https://swr.vercel.app/ for more documentation about configuration options * * ```tsx * const { data, error, isLoading, mutate } = useServerAction(() => action(id), [id], { shouldFetch: true }); * * if (isLoading) return <div>Loading...</div>; * if (error) return <div>Error: {error.message}</div>; * if (!data) return null; * * return <div>{JSON.stringify(data)}</div>; * ``` */ export default function useServerAction<T>( action: Action<T>, deps: DependencyList = [], options: UseServerActionSWROptions = {} ) { const key = generateKey(action, deps); const { shouldFetch = true, shouldRefresh = !!options.refreshInterval, onSuccess, onError, persist = false, ...rest } = options; const { mutate } = useSWRConfig(); const [isPersistedLoading, setIsPersistedLoading] = useState(true); /** * Sync fallback data with what we have persisted in localStorage */ useEffect(() => { if (!persist) { return; } const data = localStorage.getItem(key); if (data) { mutate(key, JSON.parse(data), { revalidate: false, }); /** * The default `isLoading` state of SWR only considers the * initial data fetching state. We need to manually set the * loading state to false once we have loaded in the persisted * data. */ setIsPersistedLoading(false); } }, []); const response = useSWR(shouldFetch ? key : null, action, { ...rest, refreshInterval: shouldRefresh ? options.refreshInterval : undefined, onSuccess: (data, key, config) => { if (persist) { localStorage.setItem(key, JSON.stringify(data)); } onSuccess?.(data, key, config); }, onError: (err, key, config) => { if (persist) { localStorage.removeItem(key); } onError?.(err, key, config); }, }); return { ...response, ...(persist ? { isLoading: isPersistedLoading, } : {}), }; }