Simple and complete React hooks testing utilities that encourage good testing practices.
—
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.
The server renderer provides the same renderHook API as other renderers but with additional server-specific functionality, including the ability to hydrate rendered components.
/**
* Renders a React hook in a server environment with hydration support
* @param callback - Function that calls the hook to be tested
* @param options - Optional configuration including initial props and wrapper component
* @returns ServerRenderHookResult with hydrate method and standard utilities
*/
function renderHook<TProps, TResult>(
callback: (props: TProps) => TResult,
options?: RenderHookOptions<TProps>
): ServerRenderHookResult<TProps, TResult>;
interface ServerRenderHookResult<TProps, TValue> extends RenderHookResult<TProps, TValue> {
/** Hydrate the server-rendered markup on the client */
hydrate(): void;
}Usage Examples:
import { renderHook } from "@testing-library/react-hooks/server";
import { useState, useEffect } from "react";
// Server-side hook testing
function useServerState(initialValue: string) {
const [value, setValue] = useState(initialValue);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true); // This only runs on the client
}, []);
return { value, setValue, isClient };
}
test("server-side rendering", () => {
const { result, hydrate } = renderHook(() => useServerState("server"));
// On the server, useEffect hasn't run yet
expect(result.current.value).toBe("server");
expect(result.current.isClient).toBe(false);
// Hydrate on the client
hydrate();
// After hydration, client-side effects run
expect(result.current.isClient).toBe(true);
});The hydrate() method simulates the client-side hydration of server-rendered content, allowing you to test the transition from server to client rendering.
/**
* Hydrate the server-rendered markup on the client side
* This triggers client-side effects and state updates
*/
hydrate(): void;Usage Examples:
function useHydrationEffect() {
const [serverData, setServerData] = useState("server-data");
const [clientData, setClientData] = useState(null);
useEffect(() => {
// This only runs on the client after hydration
setClientData("client-data");
}, []);
return { serverData, clientData };
}
test("hydration effects", () => {
const { result, hydrate } = renderHook(() => useHydrationEffect());
// Server state
expect(result.current.serverData).toBe("server-data");
expect(result.current.clientData).toBe(null);
// Hydrate to client
hydrate();
// Client effects have run
expect(result.current.clientData).toBe("client-data");
});
// Testing hydration mismatches
function useConditionalRender() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
// This creates a hydration mismatch
return isClient ? "client" : "server";
}
test("hydration mismatch handling", () => {
const { result, hydrate } = renderHook(() => useConditionalRender());
expect(result.current).toBe("server");
// Hydration will cause a mismatch but should resolve
hydrate();
expect(result.current).toBe("client");
});Testing hooks that depend on server-side context or data fetching scenarios.
const ServerContext = React.createContext({ isServer: true });
function useServerContext() {
const context = React.useContext(ServerContext);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return { ...context, mounted };
}
test("server context with hydration", () => {
const serverWrapper = ({ children }) => (
<ServerContext.Provider value={{ isServer: true }}>
{children}
</ServerContext.Provider>
);
const clientWrapper = ({ children }) => (
<ServerContext.Provider value={{ isServer: false }}>
{children}
</ServerContext.Provider>
);
const { result, hydrate } = renderHook(() => useServerContext(), {
wrapper: serverWrapper
});
// Server state
expect(result.current.isServer).toBe(true);
expect(result.current.mounted).toBe(false);
// Hydrate with client context
hydrate();
// Client state after hydration and effects
expect(result.current.mounted).toBe(true);
});Testing hooks that handle server-side data fetching and client-side rehydration.
function useSSRData(url: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [isSSR, setIsSSR] = useState(true);
useEffect(() => {
setIsSSR(false);
if (!data) {
// Client-side data fetching
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
});
} else {
// Data was server-rendered
setLoading(false);
}
}, [url, data]);
return { data, loading, isSSR };
}
test("SSR data fetching", async () => {
// Mock server-rendered data
const serverData = { id: 1, name: "Server Data" };
const { result, hydrate, waitFor } = renderHook(() => {
// Simulate server-side data injection
const [initialData] = useState(serverData);
return useSSRData("/api/data");
});
// Server state with pre-rendered data
expect(result.current.isSSR).toBe(true);
expect(result.current.loading).toBe(true);
// Hydrate on client
hydrate();
// Wait for client-side effects
await waitFor(() => !result.current.isSSR);
expect(result.current.isSSR).toBe(false);
expect(result.current.loading).toBe(false);
});Testing error boundaries that behave differently on server vs client.
function useSSRErrorBoundary() {
const [shouldError, setShouldError] = useState(false);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (shouldError && isClient) {
throw new Error("Client-side error");
}
return { shouldError, setShouldError, isClient };
}
test("SSR error boundary behavior", () => {
const { result, hydrate } = renderHook(() => useSSRErrorBoundary());
expect(result.current.isClient).toBe(false);
// Set error flag on server (no error thrown)
act(() => {
result.current.setShouldError(true);
});
expect(result.error).toBeUndefined();
// Hydrate to client (error now thrown)
expect(() => {
hydrate();
}).toThrow("Client-side error");
expect(result.error).toBeInstanceOf(Error);
});Test Both Server and Client States:
test("complete SSR lifecycle", () => {
const { result, hydrate } = renderHook(() => useMySSRHook());
// Verify server state
expect(result.current.serverProperty).toBeDefined();
// Hydrate and verify client state
hydrate();
expect(result.current.clientProperty).toBeDefined();
});Handle Hydration Timing:
test("hydration timing", async () => {
const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncSSRHook());
hydrate();
// Wait for async effects after hydration
await waitForNextUpdate();
expect(result.current.hydratedData).toBeDefined();
});Mock Server Environment:
test("mock server environment", () => {
// Mock server-specific globals or imports
const originalWindow = global.window;
delete global.window;
const { result, hydrate } = renderHook(() => useWindowDetection());
expect(result.current.hasWindow).toBe(false);
// Restore window for client hydration
global.window = originalWindow;
hydrate();
expect(result.current.hasWindow).toBe(true);
});Install with Tessl CLI
npx tessl i tessl/npm-testing-library--react-hooks