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:
- Initially,
isLoading
is set totrue
. - If cached data is available,
isLoading
is immediately set tofalse
. - During the actual fetch,
isLoading
remainsfalse
. You should look atisValidating
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"use client";
import { DependencyList, useEffect, useState } from "react";
import useSWR, { SWRConfiguration, useSWRConfig } from "swr";
type Action<T> = () => Promise<T>;
interface UseServerActionSWROptions<T> extends SWRConfiguration<T> {
/**
* Conditionally control when to execute the server action.
*/
shouldFetch?: boolean;
/**
* Conditionally control whether to refresh 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("&")}`;
}
type UseServerActionResponse<T> = ReturnType<typeof useSWR<T>>;
export default function useServerAction<T>(
action: Action<T>,
deps: DependencyList
): UseServerActionResponse<T>;
export default function useServerAction<T>(
action: Action<T>,
options: UseServerActionSWROptions<T>,
deps: DependencyList
): UseServerActionResponse<T>;
/**
* 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), { shouldFetch: true }, [id]);
*
* 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>,
optionsOrDeps?: UseServerActionSWROptions<T> | DependencyList,
optionalDeps: DependencyList = []
) {
let options: UseServerActionSWROptions<T> = {},
deps = optionalDeps;
if (!Array.isArray(optionsOrDeps)) {
options = optionsOrDeps as UseServerActionSWROptions<T>;
deps = optionalDeps;
} else {
deps = optionsOrDeps;
}
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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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,
}
: {}),
};
}