-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
<Provider>
misses state changes that occur between when its constructor runs and when it mounts
#1126
Comments
I'll look at this over the next couple days, but I see one key sentence that I think is the heart of the issue:
In v5, each connected component subscribed directly to the store, and called However, that behavior is actually not desirable for the long-term. The React team (Andrew specifically) uses the word "tearing" to refer to cases where different parts of a component tree see different state values during the same render pass. While that's primarily a concern in the future concurrent React world, this loophole could also be classified as "tearing". For v6, we've switched to putting the entire store state into new context, which means React guarantees that the entire component tree will see the same state value during a given render pass. So, even if parent components dispatch actions during construction, their children will still be constructed using the original state value. Now, the v6 // Actions might have been dispatched between render and mount - handle those
const postMountStoreState = store.getState()
if (postMountStoreState !== this.state.storeState) {
this.setState({ storeState: postMountStoreState })
} That technique came directly from @bvaughn's I know that someone else was complaining about dynamic reducer-type behavior in #935 . There may be something we can do to help with that scenario. But, the idea of a single consistent state value for a given render is a key aspect of v6's design. As I said, I wrote all that without having actually dug into your specific issue in depth. I'll try to look at this in more detail over the weekend. |
(side note: I greatly appreciate the detailed issue writeup and the documented CodeSandbox example - thank you for that!) |
@markerikson You bet!
Unfortunately it's already too late when I agree that v6's behavior totally makes sense given where React is headed. I'd like to find a way to fix this without needing to go back to v5's approach, but so far I'm stumped. I've spent the last two days trying to think of a way to fix this, either in React Redux or in my own app, and I'm still at a loss, so I'm hoping someone else will have a bright idea. I could do it if we didn't need SSR, or if async SSR were possible, but so far I haven't been able to come up with a way to support both synchronous SSR and dynamically attached reducers at any level of the component tree with v6. |
To be clear from React's point of view, a library "missing" an update is always a bug. No matter when an update happens, the end result should be consistent. |
It sounds like the key issue here is that these components are expecting to dynamically mount an associated reducer (which appears to be happening in another parent HOC), and immediately be getting the updated state tree before their own In a way, the fact that this ever worked in earlier versions seems like a quirk of the implementation. Note that I'm not saying that dynamic reducer loading is a bad idea or anything. It's obviously a valuable use case, and I want to find a way to support it. One potential workaround, for now, would perhaps be to "just" make your Although, having said that... SSR generally is just a single render pass, right? That... could complicate things. |
Yeah, exactly. Even if I did take this approach (which would mean refactoring dozens and dozens of components, but I'd do it if it would help!), it wouldn't solve the problem because we only get one render pass on the server. |
Is your CodeSandbox actually representative of an SSR setup? I've never done any SSR myself. It looked like you were actually doing two renders in that case, but I guess on the server you'd be calling |
No, the CodeSandbox is somewhat contrived because reproducing a full SSR setup would have made it a lot more complicated and I was trying to keep it simple. But the approach shown there is very similar (minus the actual server rendering and routing) to the approach we use in our app. And it needs to work the same on both the server and the client in any case, so I think reproducing the issue on the client still makes for a valid repro case. |
Right, I was mostly trying to understand the notion of calling Actually, now I'm curious. What does happen with |
Sorry for the confusion. The only reason for the second render in the repro case is to simulate a client-side route change that might cause a component to be dynamically loaded. It's not something you would do as shown in an app. Probably just unnecessary complexity. If you remove the first render, the issue will still occur: async function main() {
let Route = await loadRoute();
renderApp(<Route />);
}
There's never a valid opportunity to call |
Hmm. Yeah, this does seem tough. I'll admit that at first glance at the moment, I don't see a way to make this work for SSR with v6 and new context. Fundamentally, that one render pass is only ever going to see whatever is in the initial store state. On the client, if the components can survive that first Am I correct that the general idea here is to SSR-render a bunch of dynamically loaded components+reducers as part of the initial render? Is it perhaps possible to add those to the store ahead of the |
Yeah, but if we only cared about client-side rendering, we wouldn't need to use this approach at all. There are much easier ways of dealing with dynamically loaded components if you don't care about SSR. 😄
As part of the only render, yep (because SSR only gets one shot). But we have to be able to load and render the exact same components on the server and on the client, which is why we can't use two different approaches.
Nope. That would require knowing about all the components that have been loaded before we render them. If we were only loading a flat list of connected components, this would be doable. But we're actually loading a tree. The components we load can themselves import components, which can import more components, and so on. In other words, at the top level we can That's why the components themselves need to be able to attach their own reducers during the render phase; there's not really any other opportunity to do it. |
Really stupid question: could you do something like this? const dummyElement = jsDomOrSomething.createElement("div");
// dummy render to let the store update itself
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
dummyElement
);
const html = ReactServerOrSomething.renderToString(
<Provider store={store}>
<App />
</Provider>
); |
Hmm. Maybe? But that's one render for the price of two on every server response and every client-side route change, so it'd be a serious performance killer. And since I think you're probably right that there's not a good way to make this work in v6 without reintroducing the "tearing" problem you mentioned earlier. But it does seem like tearing is preferable to the alternatives. And v6 does include the store itself in the new context object. Could It seems like that could work here: react-redux/src/components/connectAdvanced.js Lines 200 to 214 in 595ae8f
That might make it possible to support this use case on an opt-in basis. |
Yeah, I know it was a bad suggestion, but I had to throw it out anyway :) Hmm. Interesting idea. Okay, lemme throw out something of a notional hack-y API suggestion. Maybe we support something like: <Provider store={store} UNSAFE_readLatestStoreStateOnFirstRender={true}> And then in the component: let { storeState, store, readStoreStateOnFirstRender } = value
let wrapperProps = this.props
let forwardedRef
if (forwardRef) {
wrapperProps = this.props.wrapperProps
forwardedRef = this.props.forwardedRef
}
if(readStoreStateOnFirstRender && this.isFirstRender) {
storeState = store.getState()
}
this.isFirstRender = false
let derivedProps = this.selectDerivedProps(
storeState,
wrapperProps,
store
) Thoughts? |
how about we make Provider SSR-aware, and then do the stuff needed if it is explicitly told that we are server-rendering? tearing is impossible in SSR, so if we are certain it's safe, this could be done in something like UNSAFE_cwrp or even in gDSFP |
The issue here isn't entirely SSR-specific, per the earlier discussion. There's issues with dynamically loading reducers and components on the client, too, where this option would be useful. |
Yeah, I think that could work well! |
Okay. I've got some other priorities atm, but if you wanted to submit a PR that implements something like that, we can certainly take a look at it. |
Definitely. I'll work on this tomorrow. Thanks for talking this through with me! |
@rgrove Fantastic writeup of this issue! 👏 Great discussion all around, I thought I'd add a few things. To me, conceptually this previously worked precisely because of finely controlling the ability to tear so that the tear always happened before any components that relied on it was rendered. Server rendering is really finicky. For anyone that has not worked on it before, I tend to recommend to think of it in terms of I can see a couple of approaches to this problem, most has already been mentioned. The first two are the same as we do for data fetching on the server side today:
Since this is an escape hatch, should it be so tightly coupled to first render or should this be up to the consumer to decide (in case it is needed for other now unknown usecases)? The first render-part should be easy to implement in userspace. <Provider store={store} UNSAFE_readLatestStoreState={isFirstRender}>
// or
<Provider store={store} UNSAFE_readLatestStoreState={isServer}> This seems more flexible but might be slightly harder to use. Not sure what's best, just thinking out loud. :) |
Actually:
Son is waking up so can't write this up more right now, but wanted to dump it in here since it just struck me as something that might be possible? |
Excellent discussion! I'll try in a first step to implement the Regarding Thank you, |
Hmm, having a snack with the son so throwing another quick one in here:
|
I've just created a PR with the On the server, I have logs like:
On the client side:
The However, we force the call to Regarding solution Hope it will help ;) |
I think this is a much broader problem then described. We also have an SSR-compatible app that stopped working with the upgrade to the latest react-redux. I've tried to refactor its architecture thinking that maybe something is wrong with it but eventually gave up. The main reason is the root component misses state changes. I've added a test prop to the root component too see whether it changes on state updates: export default compose(
withRouter,
connect(
({ app }) => ({
...app,
foo: Math.random()
}),
dispatch => ({
loadOnClient: (source, clientDataLoader) => clientDataLoader(source, dispatch)
})
)
)(App); and then I've dispatched an action from within a nested connected component in a way setTimeout(() => {
// dispatch action here
}, 1000) just to be sure that the whole app tree is rendered to the time of the call. The new state value will be passed on the next state update. So it kind of lags in one update. No idea how to solve this problem, I suppose we stick to 5.x for good. |
@AgentCoop let's not give up just yet. Everyone will need this to be solved, including me, so we just have to figure out the right question to ask and then a solution will present itself. I may have time to take a look later today. It would help a lot if you could forward the codesandbox above and modify it to fit the test case you have described. Do you think that is possible? Thanks! |
Since https://codesandbox.io/s/pwmm8r3110 I think this solution makes sense. Rationale (& somewhat of a summary) Sometimes (seldom) it makes sense to update the state within a render-pass, such as dispatching an action in This special case requires the update to happen within the same render-pass, both in the sandboxed client-side version and any serverside solution. The reason this is safe to do at all, is because of the constraint that only children of the component that performs the change relies on that change (or children in different parts of the tree whose parents all introduce the same change). In order to support concurrent rendering and avoid tearing, v6 no longer fetches the current Redux store-state in every connect-component, instead relying on the version that is stored on context. Because of the above constraint however, we only have to update the context of the subtree that relies on this change, and thankfully React provides a way to do this. Downsides Downsides of this solution is a heavier implementation-burden in userland. This would be a unintended/unplanned for breaking change (of an undocumented implementation detail, so not sure it counts..). Very possible it has other downsides I can't come up with right now. :) That being said, I'm hesitant to if it's a good idea to introduce a long term escape hatch for tearing unless it is strictly needed, this feels like a more idiomatic solution. |
@Ephem: I have to say this seems like the best solution I've seen so far. |
Thanks for all the great suggestions everyone! 😄 I tried out @Ephem's solution and it's working great. This definitely feels like the right way to go if we want to solve this in userland. @markerikson I'm also still happy to work on a PR if you think it's worth solving this in the library (although @GuillaumeCisco's PR looks pretty close to what I'd implement, so you could just take that one). But if you'd rather we stick to a userland fix, I'm happy with that too. |
I'm glad you found the problem! 🎉 With that I think we can consider this issue resolved. If anyone read this and think they have an issue related to this one (code splitting with |
Yep. I'd really appreciate it if someone could take the time to write up a new React-Redux docs page on this topic. Maybe focus it specifically on the code-splitting aspect for now, and we can generalize the "here's how to access the store if you really need to" part later. |
Comparing what @rgrove did with the Provider's code, I have a related question. He initially supplies the Provider with a store, but keeps modifying the store (by adding reducers dynamically) outside the Provider. Inside the Provider, the store is kept in its state. I think the right approach would be to have a handler in the Provider that takes a new store as parameter and call setState with the new store. Am I missing something? |
@bumi001 : uh... no. The point of this discussion is to keep using the same Redux store reference, but update it with a new reducer function (and thus a new store state value). No one said anything about creating or using a different store reference. Why are you bringing that up? |
Comparing what @rgrove did with the Provider's code, I have a related question. He initially supplies the Provider with a store, but keeps modifying the store (by adding reducers dynamically) outside the Provider. Inside the Provider, the store is kept in its state. I think the right approach would be to have a handler in the Provider that takes a new store as parameter and call setState with the new store. I would add this handler to the value supplied to Context.Provider. Am I missing something? |
It looks to me that the store is passed down via the Provider and through the AppContext to child components. Since Provider is the parent of App and all its children, in order to be consistent with React's design, if you want to change the state of the parent then you need to do it via a handler than an external reference. I could also keep an external reference to something in the store and change it via that instead of dispatching an action. But, that would be inconsistent with redux's design. |
@bumi001 : I'm... really not sure what you're trying to say. Seriously. What point are you trying to make / question are you trying to ask? |
The render function in Provider is
} I am interested in dynamically injecting reducers. I do it like below, which is a simplified version of what is in react-boilerplate. import React from 'react'; export default ({ key, reducer }) => WrappedComponent => {
} return hoistNonReactStatics(ReducerInjector, WrappedComponent); The function createReducer calls combineReducers. The difference from rgrove's code is that I get the store from ReactReduxContext.Consumer. My question is, if I update the store this way, is it going to change/update the store and storeState in Provider? |
store.injectedReducers[key] = reducer; //I had a typo |
Yes, because the store dispatches an action when you call |
I see. Got it. Thank you. I didn't look into replaceReducer. I saw the subscribe in Provider. |
In the context of react-boilerplate, I do the following to deal with the initialization issue: import { fromJS } from 'immutable'; export const initialState = fromJS({ (2) import initialState in selectors.js and use it like: import { initialState } from './reducer'; //immutable.js map returns initialState if 'home' is not found in state (3) where 'home' is the key given to injectReducer in index.js const withReducer = injectReducer({ key: 'home', reducer }); |
Here is my PR for react-boilerplate that uses react-redux:6.0.0 and connected-react-router:6.1.0 if anyone is interested. It passes all their tests. |
Do you want to request a feature or report a bug?
Bug
What is the current behavior?
If state changes occur during the time after
<Provider>
's constructor runs and before<Provider>
is mounted,<Provider>
will miss those changes and won't make them available to connected components during the render phase of those components.This is probably a rare use case, but it can occur in a very specific scenario that's useful in an app that uses both server-side and client rendering and needs to allow dynamically loaded components (such as components loaded when the route changes) to attach new reducers to an existing store.
Here's a reduced test case that fails in React Redux 6.0: https://codesandbox.io/s/612k3pv1yz
What is the expected behavior?
Connected components should always see the most recent Redux state, even if that state changed between when
<Provider>
was constructed and when it was mounted. This was the behavior in 5.x.Here's an exact copy of the reduced test case above, but using React Redux 5.1.1 to demonstrate that this works fine there: https://codesandbox.io/s/yvw1vmnkrv
Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?
Redux 4.0.1. This worked in React Redux 5.x, but broke in 6.0. It's unrelated to any specific browser or OS.
More background on this use case
I realize this use case may be a little hard to understand at first glance, so I'll try to explain in more detail why it's valuable.
The repro case above simulates a technique that's useful when using Redux and React Redux in an SSR-compatible app that needs to be able to load components and attach new reducers on demand (such as when the route changes) without knowing up front what components or reducers might eventually be loaded.
This technique is used extensively by Cake. I believe New Twitter uses a similar approach, but they're still on React Redux 4.x so aren't yet affected by this bug.
When rendering on the server, we can't load components during the render phase (because the SSR render phase is synchronous). So instead we need to load all components for a given render pass up front, then attach reducers as needed during the render phase of the app.
Dynamically loaded components may themselves import other components, and components at any level of the import tree could be Redux-connected. This means each component must be able to attach its own reducers. The
withRedux
decorator in the repro case simplifies this by wrapping a dynamically loaded component (such as theRoute
component in the example) in a higher order component that attaches reducers on demand in its constructor.Since React constructs
<Provider>
before it constructs its children, this means that the Redux store<Provider>
sees at construction time doesn't yet have a complete set of reducers attached.Once the child components are constructed, all reducers will have been attached and React will begin to render the component tree, but
<Provider>
in React Redux 6.0 passes the old state to all its context consumers during the render phase, breaking any component that expects the new state.<Provider>
doesn't check for state changes untilcomponentDidMount()
runs, at which point it's already too late.Edit: s/Redux/React Redux/ in various places because I'm tired. I do know the difference, I promise!
Edit 2: Clarified that dynamically loaded reducers are attached during the render phase, not before it.
The text was updated successfully, but these errors were encountered: