-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[v7-beta] Add test for dynamically injecting reducers #1211
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', () => { | ||
// 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() | ||
}) | ||
}) | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
.