Skip to content

Commit 79982c9

Browse files
Ephemtimdorr
authored andcommitted
Added test for injecting dynamic reducers on client and server (#1211)
1 parent 1d6ccc1 commit 79982c9

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*eslint-disable react/prop-types*/
2+
3+
import React from 'react'
4+
import ReactDOMServer from 'react-dom/server'
5+
import { createStore, combineReducers } from 'redux'
6+
import { connect, Provider, ReactReduxContext } from '../../src/index.js'
7+
import * as rtl from 'react-testing-library'
8+
9+
describe('React', () => {
10+
/*
11+
For SSR to work, there are three options for injecting
12+
dynamic reducers:
13+
14+
1. Make sure all dynamic reducers are known before rendering
15+
(requires keeping knowledge about this outside of the
16+
React component-tree)
17+
2. Double rendering (first render injects required reducers)
18+
3. Inject reducers as a side effect during the render phase
19+
(in construct or render), and try to control for any
20+
issues with that. This requires grabbing the store from
21+
context and possibly patching any storeState that exists
22+
on there, these are undocumented APIs that might change
23+
at any time.
24+
25+
Because the tradeoffs in 1 and 2 are quite hefty and also
26+
because it's the popular approach, this test targets nr 3.
27+
*/
28+
describe('dynamic reducers', () => {
29+
const InjectReducersContext = React.createContext(null)
30+
31+
function ExtraReducersProvider({ children, reducers }) {
32+
return (
33+
<InjectReducersContext.Consumer>
34+
{injectReducers => (
35+
<ReactReduxContext.Consumer>
36+
{reduxContext => {
37+
const latestState = reduxContext.store.getState()
38+
const contextState = reduxContext.storeState
39+
40+
let shouldInject = false
41+
let shouldPatch = false
42+
43+
for (const key of Object.keys(reducers)) {
44+
// If any key does not exist in the latest version
45+
// of the state, we need to inject reducers
46+
if (!(key in latestState)) {
47+
shouldInject = true
48+
}
49+
// If state exists on the context, and if any reducer
50+
// key is not included there, we need to patch it up
51+
// Only patching if storeState exists makes this test
52+
// work with multiple React-Redux approaches
53+
if (contextState && !(key in contextState)) {
54+
shouldPatch = true
55+
}
56+
}
57+
58+
if (shouldInject) {
59+
injectReducers(reducers)
60+
}
61+
62+
if (shouldPatch) {
63+
// A safer way to do this would be to patch the storeState
64+
// manually with the state from the new reducers, since
65+
// this would better avoid tearing in a future concurrent world
66+
const patchedReduxContext = {
67+
...reduxContext,
68+
storeState: reduxContext.store.getState()
69+
}
70+
return (
71+
<ReactReduxContext.Provider value={patchedReduxContext}>
72+
{children}
73+
</ReactReduxContext.Provider>
74+
)
75+
}
76+
77+
return children
78+
}}
79+
</ReactReduxContext.Consumer>
80+
)}
81+
</InjectReducersContext.Consumer>
82+
)
83+
}
84+
85+
const initialReducer = {
86+
initial: (state = { greeting: 'Hello world' }) => state
87+
}
88+
const dynamicReducer = {
89+
dynamic: (state = { greeting: 'Hello dynamic world' }) => state
90+
}
91+
92+
function Greeter({ greeting }) {
93+
return <div>{greeting}</div>
94+
}
95+
96+
const InitialGreeting = connect(state => ({
97+
greeting: state.initial.greeting
98+
}))(Greeter)
99+
const DynamicGreeting = connect(state => ({
100+
greeting: state.dynamic.greeting
101+
}))(Greeter)
102+
103+
function createInjectReducers(store, initialReducer) {
104+
let reducers = initialReducer
105+
return function injectReducers(newReducers) {
106+
reducers = { ...reducers, ...newReducers }
107+
store.replaceReducer(combineReducers(reducers))
108+
}
109+
}
110+
111+
let store
112+
let injectReducers
113+
114+
beforeEach(() => {
115+
// These could be singletons on the client, but
116+
// need to be separate per request on the server
117+
store = createStore(combineReducers(initialReducer))
118+
injectReducers = createInjectReducers(store, initialReducer)
119+
})
120+
121+
it('should render child with initial state on the client', () => {
122+
const { getByText } = rtl.render(
123+
<Provider store={store}>
124+
<InjectReducersContext.Provider value={injectReducers}>
125+
<InitialGreeting />
126+
<ExtraReducersProvider reducers={dynamicReducer}>
127+
<DynamicGreeting />
128+
</ExtraReducersProvider>
129+
</InjectReducersContext.Provider>
130+
</Provider>
131+
)
132+
133+
getByText('Hello world')
134+
getByText('Hello dynamic world')
135+
})
136+
it('should render child with initial state on the server', () => {
137+
// In order to keep these tests together in the same file,
138+
// we aren't currently rendering this test in the node test
139+
// environment
140+
// This generates errors for using useLayoutEffect in v7
141+
// We hide that error by disabling console.error here
142+
143+
jest.spyOn(console, 'error')
144+
// eslint-disable-next-line no-console
145+
console.error.mockImplementation(() => {})
146+
147+
const markup = ReactDOMServer.renderToString(
148+
<Provider store={store}>
149+
<InjectReducersContext.Provider value={injectReducers}>
150+
<InitialGreeting />
151+
<ExtraReducersProvider reducers={dynamicReducer}>
152+
<DynamicGreeting />
153+
</ExtraReducersProvider>
154+
</InjectReducersContext.Provider>
155+
</Provider>
156+
)
157+
158+
expect(markup).toContain('Hello world')
159+
expect(markup).toContain('Hello dynamic world')
160+
161+
// eslint-disable-next-line no-console
162+
console.error.mockRestore()
163+
})
164+
})
165+
})

0 commit comments

Comments
 (0)