Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v7-beta] Add test for dynamically injecting reducers #1211

Merged
merged 1 commit into from
Mar 22, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions test/integration/dynamic-reducers.spec.js
Original file line number Diff line number Diff line change
@@ -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 (
<InjectReducersContext.Consumer>
{injectReducers => (
<ReactReduxContext.Consumer>
{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 (
<ReactReduxContext.Provider value={patchedReduxContext}>
{children}
</ReactReduxContext.Provider>
)
}

return children
}}
</ReactReduxContext.Consumer>
)}
</InjectReducersContext.Consumer>
)
}

const initialReducer = {
initial: (state = { greeting: 'Hello world' }) => state
}
const dynamicReducer = {
dynamic: (state = { greeting: 'Hello dynamic world' }) => state
}

function Greeter({ greeting }) {
return <div>{greeting}</div>
}

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(
<Provider store={store}>
<InjectReducersContext.Provider value={injectReducers}>
<InitialGreeting />
<ExtraReducersProvider reducers={dynamicReducer}>
<DynamicGreeting />
</ExtraReducersProvider>
</InjectReducersContext.Provider>
</Provider>
)

getByText('Hello world')
getByText('Hello dynamic world')
})
it('should render child with initial state on the server', () => {
Copy link
Contributor Author

@Ephem Ephem Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly the test-environment can only differ between files, not single tests. If you think it makes more sense, I'll refactor the implementation into a util-file and move this test over to the SSR-suite where the true node test-environment is running, or a new dynamic-reducers-server.spec.js.

// 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(
<Provider store={store}>
<InjectReducersContext.Provider value={injectReducers}>
<InitialGreeting />
<ExtraReducersProvider reducers={dynamicReducer}>
<DynamicGreeting />
</ExtraReducersProvider>
</InjectReducersContext.Provider>
</Provider>
)

expect(markup).toContain('Hello world')
expect(markup).toContain('Hello dynamic world')

// eslint-disable-next-line no-console
console.error.mockRestore()
})
})
})