diff --git a/test/integration/dynamic-reducers.spec.js b/test/integration/dynamic-reducers.spec.js new file mode 100644 index 000000000..dc2e3561b --- /dev/null +++ b/test/integration/dynamic-reducers.spec.js @@ -0,0 +1,165 @@ +/*eslint-disable react/prop-types*/ + +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import { createStore, combineReducers } from 'redux' +import { connect, Provider, ReactReduxContext } from '../../src/index.js' +import * as rtl from 'react-testing-library' + +describe('React', () => { + /* + For SSR to work, there are three options for injecting + dynamic reducers: + + 1. Make sure all dynamic reducers are known before rendering + (requires keeping knowledge about this outside of the + React component-tree) + 2. Double rendering (first render injects required reducers) + 3. Inject reducers as a side effect during the render phase + (in construct or render), and try to control for any + issues with that. This requires grabbing the store from + context and possibly patching any storeState that exists + on there, these are undocumented APIs that might change + at any time. + + Because the tradeoffs in 1 and 2 are quite hefty and also + because it's the popular approach, this test targets nr 3. + */ + describe('dynamic reducers', () => { + const InjectReducersContext = React.createContext(null) + + function ExtraReducersProvider({ children, reducers }) { + return ( + + {injectReducers => ( + + {reduxContext => { + const latestState = reduxContext.store.getState() + const contextState = reduxContext.storeState + + let shouldInject = false + let shouldPatch = false + + for (const key of Object.keys(reducers)) { + // If any key does not exist in the latest version + // of the state, we need to inject reducers + if (!(key in latestState)) { + shouldInject = true + } + // If state exists on the context, and if any reducer + // key is not included there, we need to patch it up + // Only patching if storeState exists makes this test + // work with multiple React-Redux approaches + if (contextState && !(key in contextState)) { + shouldPatch = true + } + } + + if (shouldInject) { + injectReducers(reducers) + } + + if (shouldPatch) { + // A safer way to do this would be to patch the storeState + // manually with the state from the new reducers, since + // this would better avoid tearing in a future concurrent world + const patchedReduxContext = { + ...reduxContext, + storeState: reduxContext.store.getState() + } + return ( + + {children} + + ) + } + + return children + }} + + )} + + ) + } + + const initialReducer = { + initial: (state = { greeting: 'Hello world' }) => state + } + const dynamicReducer = { + dynamic: (state = { greeting: 'Hello dynamic world' }) => state + } + + function Greeter({ greeting }) { + return
{greeting}
+ } + + const InitialGreeting = connect(state => ({ + greeting: state.initial.greeting + }))(Greeter) + const DynamicGreeting = connect(state => ({ + greeting: state.dynamic.greeting + }))(Greeter) + + function createInjectReducers(store, initialReducer) { + let reducers = initialReducer + return function injectReducers(newReducers) { + reducers = { ...reducers, ...newReducers } + store.replaceReducer(combineReducers(reducers)) + } + } + + let store + let injectReducers + + beforeEach(() => { + // These could be singletons on the client, but + // need to be separate per request on the server + store = createStore(combineReducers(initialReducer)) + injectReducers = createInjectReducers(store, initialReducer) + }) + + it('should render child with initial state on the client', () => { + const { getByText } = rtl.render( + + + + + + + + + ) + + getByText('Hello world') + getByText('Hello dynamic world') + }) + it('should render child with initial state on the server', () => { + // In order to keep these tests together in the same file, + // we aren't currently rendering this test in the node test + // environment + // This generates errors for using useLayoutEffect in v7 + // We hide that error by disabling console.error here + + jest.spyOn(console, 'error') + // eslint-disable-next-line no-console + console.error.mockImplementation(() => {}) + + const markup = ReactDOMServer.renderToString( + + + + + + + + + ) + + expect(markup).toContain('Hello world') + expect(markup).toContain('Hello dynamic world') + + // eslint-disable-next-line no-console + console.error.mockRestore() + }) + }) +})