Skip to content

Commit 845665d

Browse files
committed
fix(Provider): throw actionable error when rendered in a React Server Component (reduxjs#2082)
1 parent dc531f4 commit 845665d

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

src/components/Provider.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ export interface ProviderProps<
5757
function Provider<A extends Action<string> = UnknownAction, S = unknown>(
5858
providerProps: ProviderProps<A, S>,
5959
) {
60+
if (process.env.NODE_ENV !== 'production') {
61+
// React Server Components do not provide the effect hooks that `<Provider>`
62+
// relies on, so `useIsomorphicLayoutEffect` (which resolves to
63+
// `useLayoutEffect`/`useEffect`) is `undefined` in that environment. Detect
64+
// this before any hook runs and throw an actionable error, instead of the
65+
// cryptic "X is not a function" that would otherwise surface.
66+
if (typeof useIsomorphicLayoutEffect !== 'function') {
67+
throw new Error(
68+
'The React Redux `<Provider>` component requires React hooks that are ' +
69+
'not available in a React Server Component. This usually means you are ' +
70+
'rendering `<Provider>` in a Server Component. Add the `"use client"` ' +
71+
'directive to the top of the file that renders `<Provider>` (or a ' +
72+
'parent that ends up rendering it) so it is treated as a Client ' +
73+
'Component. See https://react.dev/reference/rsc/use-client for more ' +
74+
'information.',
75+
)
76+
}
77+
}
78+
6079
const { children, context, serverState, store } = providerProps
6180

6281
const contextValue = React.useMemo(() => {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*eslint-disable react/prop-types*/
2+
3+
import * as rtl from '@testing-library/react'
4+
import { Provider } from 'react-redux'
5+
import { createStore } from 'redux'
6+
7+
// In a React Server Components environment, React does not expose the effect
8+
// hooks that `<Provider>` relies on, so `useIsomorphicLayoutEffect` (which
9+
// resolves to `useLayoutEffect`/`useEffect`) ends up `undefined`. Simulate that
10+
// here to assert `<Provider>` surfaces a clear, actionable error rather than a
11+
// cryptic "X is not a function".
12+
vi.mock('../../src/utils/useIsomorphicLayoutEffect', () => ({
13+
useIsomorphicLayoutEffect: undefined,
14+
}))
15+
16+
// This mock replaces an internal source module, which only exists as a separate
17+
// module in the `src` build. When the suite runs against the bundled build
18+
// artifact (`TEST_DIST`), `useIsomorphicLayoutEffect` is inlined into a single
19+
// file with no module boundary left to mock, so the RSC condition cannot be
20+
// simulated there. The guard itself is unchanged between builds, so exercising
21+
// it against `src` is sufficient.
22+
const describeSource = process.env.TEST_DIST ? describe.skip : describe
23+
24+
describeSource('React', () => {
25+
describe('Provider in a React Server Component environment', () => {
26+
it('throws an actionable error when React effect hooks are unavailable', () => {
27+
const store = createStore(() => ({}))
28+
29+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
30+
31+
expect(() =>
32+
rtl.render(
33+
<Provider store={store}>
34+
<div />
35+
</Provider>,
36+
),
37+
).toThrow(/React Server Component/)
38+
39+
spy.mockRestore()
40+
})
41+
})
42+
})

0 commit comments

Comments
 (0)