0
# Server-Side Rendering
1
2
Server-specific rendering capabilities for testing hooks in SSR environments with hydration support. This functionality is only available when using the server renderer from `@testing-library/react-hooks/server`.
3
4
## Capabilities
5
6
### Server renderHook
7
8
The server renderer provides the same `renderHook` API as other renderers but with additional server-specific functionality, including the ability to hydrate rendered components.
9
10
```typescript { .api }
11
/**
12
* Renders a React hook in a server environment with hydration support
13
* @param callback - Function that calls the hook to be tested
14
* @param options - Optional configuration including initial props and wrapper component
15
* @returns ServerRenderHookResult with hydrate method and standard utilities
16
*/
17
function renderHook<TProps, TResult>(
18
callback: (props: TProps) => TResult,
19
options?: RenderHookOptions<TProps>
20
): ServerRenderHookResult<TProps, TResult>;
21
22
interface ServerRenderHookResult<TProps, TValue> extends RenderHookResult<TProps, TValue> {
23
/** Hydrate the server-rendered markup on the client */
24
hydrate(): void;
25
}
26
```
27
28
**Usage Examples:**
29
30
```typescript
31
import { renderHook } from "@testing-library/react-hooks/server";
32
import { useState, useEffect } from "react";
33
34
// Server-side hook testing
35
function useServerState(initialValue: string) {
36
const [value, setValue] = useState(initialValue);
37
const [isClient, setIsClient] = useState(false);
38
39
useEffect(() => {
40
setIsClient(true); // This only runs on the client
41
}, []);
42
43
return { value, setValue, isClient };
44
}
45
46
test("server-side rendering", () => {
47
const { result, hydrate } = renderHook(() => useServerState("server"));
48
49
// On the server, useEffect hasn't run yet
50
expect(result.current.value).toBe("server");
51
expect(result.current.isClient).toBe(false);
52
53
// Hydrate on the client
54
hydrate();
55
56
// After hydration, client-side effects run
57
expect(result.current.isClient).toBe(true);
58
});
59
```
60
61
### Hydration Method
62
63
The `hydrate()` method simulates the client-side hydration of server-rendered content, allowing you to test the transition from server to client rendering.
64
65
```typescript { .api }
66
/**
67
* Hydrate the server-rendered markup on the client side
68
* This triggers client-side effects and state updates
69
*/
70
hydrate(): void;
71
```
72
73
**Usage Examples:**
74
75
```typescript
76
function useHydrationEffect() {
77
const [serverData, setServerData] = useState("server-data");
78
const [clientData, setClientData] = useState(null);
79
80
useEffect(() => {
81
// This only runs on the client after hydration
82
setClientData("client-data");
83
}, []);
84
85
return { serverData, clientData };
86
}
87
88
test("hydration effects", () => {
89
const { result, hydrate } = renderHook(() => useHydrationEffect());
90
91
// Server state
92
expect(result.current.serverData).toBe("server-data");
93
expect(result.current.clientData).toBe(null);
94
95
// Hydrate to client
96
hydrate();
97
98
// Client effects have run
99
expect(result.current.clientData).toBe("client-data");
100
});
101
102
// Testing hydration mismatches
103
function useConditionalRender() {
104
const [isClient, setIsClient] = useState(false);
105
106
useEffect(() => {
107
setIsClient(true);
108
}, []);
109
110
// This creates a hydration mismatch
111
return isClient ? "client" : "server";
112
}
113
114
test("hydration mismatch handling", () => {
115
const { result, hydrate } = renderHook(() => useConditionalRender());
116
117
expect(result.current).toBe("server");
118
119
// Hydration will cause a mismatch but should resolve
120
hydrate();
121
122
expect(result.current).toBe("client");
123
});
124
```
125
126
### Server Context Testing
127
128
Testing hooks that depend on server-side context or data fetching scenarios.
129
130
```typescript
131
const ServerContext = React.createContext({ isServer: true });
132
133
function useServerContext() {
134
const context = React.useContext(ServerContext);
135
const [mounted, setMounted] = useState(false);
136
137
useEffect(() => {
138
setMounted(true);
139
}, []);
140
141
return { ...context, mounted };
142
}
143
144
test("server context with hydration", () => {
145
const serverWrapper = ({ children }) => (
146
<ServerContext.Provider value={{ isServer: true }}>
147
{children}
148
</ServerContext.Provider>
149
);
150
151
const clientWrapper = ({ children }) => (
152
<ServerContext.Provider value={{ isServer: false }}>
153
{children}
154
</ServerContext.Provider>
155
);
156
157
const { result, hydrate } = renderHook(() => useServerContext(), {
158
wrapper: serverWrapper
159
});
160
161
// Server state
162
expect(result.current.isServer).toBe(true);
163
expect(result.current.mounted).toBe(false);
164
165
// Hydrate with client context
166
hydrate();
167
168
// Client state after hydration and effects
169
expect(result.current.mounted).toBe(true);
170
});
171
```
172
173
### SSR Data Fetching
174
175
Testing hooks that handle server-side data fetching and client-side rehydration.
176
177
```typescript
178
function useSSRData(url: string) {
179
const [data, setData] = useState(null);
180
const [loading, setLoading] = useState(true);
181
const [isSSR, setIsSSR] = useState(true);
182
183
useEffect(() => {
184
setIsSSR(false);
185
186
if (!data) {
187
// Client-side data fetching
188
fetch(url)
189
.then(response => response.json())
190
.then(data => {
191
setData(data);
192
setLoading(false);
193
});
194
} else {
195
// Data was server-rendered
196
setLoading(false);
197
}
198
}, [url, data]);
199
200
return { data, loading, isSSR };
201
}
202
203
test("SSR data fetching", async () => {
204
// Mock server-rendered data
205
const serverData = { id: 1, name: "Server Data" };
206
207
const { result, hydrate, waitFor } = renderHook(() => {
208
// Simulate server-side data injection
209
const [initialData] = useState(serverData);
210
return useSSRData("/api/data");
211
});
212
213
// Server state with pre-rendered data
214
expect(result.current.isSSR).toBe(true);
215
expect(result.current.loading).toBe(true);
216
217
// Hydrate on client
218
hydrate();
219
220
// Wait for client-side effects
221
await waitFor(() => !result.current.isSSR);
222
223
expect(result.current.isSSR).toBe(false);
224
expect(result.current.loading).toBe(false);
225
});
226
```
227
228
### Error Boundaries in SSR
229
230
Testing error boundaries that behave differently on server vs client.
231
232
```typescript
233
function useSSRErrorBoundary() {
234
const [shouldError, setShouldError] = useState(false);
235
const [isClient, setIsClient] = useState(false);
236
237
useEffect(() => {
238
setIsClient(true);
239
}, []);
240
241
if (shouldError && isClient) {
242
throw new Error("Client-side error");
243
}
244
245
return { shouldError, setShouldError, isClient };
246
}
247
248
test("SSR error boundary behavior", () => {
249
const { result, hydrate } = renderHook(() => useSSRErrorBoundary());
250
251
expect(result.current.isClient).toBe(false);
252
253
// Set error flag on server (no error thrown)
254
act(() => {
255
result.current.setShouldError(true);
256
});
257
258
expect(result.error).toBeUndefined();
259
260
// Hydrate to client (error now thrown)
261
expect(() => {
262
hydrate();
263
}).toThrow("Client-side error");
264
265
expect(result.error).toBeInstanceOf(Error);
266
});
267
```
268
269
### Best Practices for SSR Testing
270
271
**Test Both Server and Client States:**
272
```typescript
273
test("complete SSR lifecycle", () => {
274
const { result, hydrate } = renderHook(() => useMySSRHook());
275
276
// Verify server state
277
expect(result.current.serverProperty).toBeDefined();
278
279
// Hydrate and verify client state
280
hydrate();
281
expect(result.current.clientProperty).toBeDefined();
282
});
283
```
284
285
**Handle Hydration Timing:**
286
```typescript
287
test("hydration timing", async () => {
288
const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncSSRHook());
289
290
hydrate();
291
292
// Wait for async effects after hydration
293
await waitForNextUpdate();
294
295
expect(result.current.hydratedData).toBeDefined();
296
});
297
```
298
299
**Mock Server Environment:**
300
```typescript
301
test("mock server environment", () => {
302
// Mock server-specific globals or imports
303
const originalWindow = global.window;
304
delete global.window;
305
306
const { result, hydrate } = renderHook(() => useWindowDetection());
307
308
expect(result.current.hasWindow).toBe(false);
309
310
// Restore window for client hydration
311
global.window = originalWindow;
312
hydrate();
313
314
expect(result.current.hasWindow).toBe(true);
315
});
316
```