-
-
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
Concurrent Mode #1351
Comments
There's still no ETA on when Concurrent Mode will be released, and there's also a general lack of clarity on what exactly it will include, what the impacts are, and what the points of incompatibility may be.
Until Concurrent Mode is actually out and completely documented, we can really only speculate on the potential interactions, and there's a lot of other stuff that I'd rather spend my time worrying about. I'll leave this open as a placeholder, but it's not an immediate priority. |
I don't mean any rush, but while it's not an immediate priority, let's do some experiment. It would be nice if someone could review it as I might be biased. |
Hello redux team, We were just wondering if there are any updated guidelines or at least high-level predictions on building components libraries on top of react-redux and making sure, to the best knowledge available, they will play nicely with concurrent mode? We have switched fully to hooks version with useSelector for reading data and useDispatch for writing. We are also making sure that we do not do any side effects during rendering, i.e. all the dispatches are done inside useEffect. To be honest - just this - to only dispatch inside useEffect - has resulted in complete refactoring of entire codebase. Since its been a few months that this issue was opened, it would be great to get some more feedback from the team about the future. Thanks for your hard work! |
@gcloeval: nope, no further updates, because there's no new info available on Concurrent Mode. Realistically, I expect React-Redux won't be "compatible", in the sense that folks expect store updates to be readable immediately. That said, it may not be a blocker from using Concurrent Mode. |
@markerikson now we have updates on concurrent mode could we see a future of this? |
The short version is that it's going to be up to the community to experiment and see exactly where the incompatibilities are. I don't currently have time to look into it myself, and I don't have enough experience doing data fetching to work with most of the concepts that are described. As an example, Dan told me that Suspense transitions would likely be problematic, because you'd end up with cases where the Redux store is already updated from a dispatched action and thus trying to display "new" UI, whereas Suspense would want to still be displaying "old" UI. Dan gave some short related answers on Redux + Suspense/CM in this SO answer. The other major issue is "tearing", where new Redux actions might be dispatched during the pauses in rendering, and thus cause different parts of the tree to read different versions of the store state during the same render pass. (The v6 implementation was intended to avoid this by passing the current store state via context for consistency, but we had to drop that approach and go back to direct subscriptions due to perf problems.) Dan has strongly recommended trying to duplicate some of the Suspense/CM demos with Redux. Someone on Reactiflux was trying to do a bit of that the other day, and I'll see if I can paste in their comments. But yeah, I'm gonna have to ask folks to go experiment with this and report their findings here. My own focus for the foreseeable future is on rewriting the Redux core docs and encouraging people to use Redux Starter Kit. |
Hi folks. |
@salvoravida : if you've got ideas, please feel free to prototype them and see how they work out. That said, I'm not sure how what you're describing is actually different than what we do right now internally, and it doesn't change the issue that there's going to be differences between what the Redux store says is the "current" state value, and what React is currently wanting to display based on Suspense changes. |
The "tearing" issue is .. expected. At least within
If the old UI needs to display "current" data, then it's a blocking-update and React will warn about it with or without using Redux. So the point is to "bake" The mechanism is pretty simple actually. This is what tells Suspense to .. suspend the UI: const MyComponent = () => throw { then(tellSuspenseThatWeAreReadyForNewUI, fail) {} }
const App = () => {
return (
<Suspense fallback="New UI not ready">
<MyComponent />
</Suspense>
)
} So, the main issue I think is .. what to This is a very naive implementation: const promise = { // throw this instead
then(tellSuspenceToContinue, throwAnError) {
const unsubscribe = store.subscribe(() => {
if (store.getState() !== STATUS.PENDING) {
ubsubscribe();
tellSuspenceToContinue();
}
});
}
}; Here is a sandbox which describes what I mentioned above: A complete separate issue is tearing that is not related to "intentional transition/suspense tearing" that could happen when nested components render too slow. However, I don't know if this is a bug or not. I found out that some slow renders are never committed to the DOM, so I'm just waiting for a more .. stable - experimental release 🙈 to know if it's really a thing to worry about or not. Currently happens because of nested Providers with react-redux. However, I haven't tried using react-redux hooks. I suspect that the issue shouldn't be if there are no connected components and just hooks but I haven't really had the time to test that. |
Related issue: JustSift/ReSift#32 |
@eddyw i can't see any unexpected behavior in your demo. on StoreState changes -> useState.setState if after startTransition, storeState changes many times it will enqueue setState many times. that's fine. Am i missing something? |
We're having a good discussion with Dan atm on Twitter about whether it would be helpful for the React team to try porting a couple of the Suspense demos to Redux to show what's broken, and and how we might be able to understand the issues involved. A few entry points into the subthreads (read up and down from these links): https://twitter.com/dan_abramov/status/1192569023529140231 |
Oh, there's so much going on here. In my perspective, the tearing issue is relatively easy. We just take the approach either by use-subscription or reactive-react-redux. (Both have slight issues, but let's put aside.) What's hard is the new pattern with Suspense and CM. Transitions and Render-as-You-Fetch pattern. This is not because of the core libraries |
hum i think we are mixing many points together. instead startTransition that will stop low priority states commit will work perfectly with redux, as @eddyw demos shows. |
I'm not sure if I'm on the same track, but what I'm suggesting is the pattern that Redux doesn't handle async functions including loading state. I'm not sure if that pattern is acceptable in the React Redux community, so that's my question. Yeah, this topic might be too broad. Here's a tiny example I have in mind. On the second thought, this example can probably be implemented with redux-thunk. |
hi, i have done some test with your demos, here are my 2 cents: import React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { unstable_runWithPriority, unstable_UserBlockingPriority } from 'scheduler';
import {
syncBlock,
useRegisterIncrementDispatcher,
reducer,
ids,
useCheckTearing,
} from '../common';
const store = createStore(reducer);
const Counter = React.memo(() => {
const count = useSelector(state => state.count);
syncBlock();
return <div className="count">{count}</div>;
});
const Main = () => {
const dispatch = useDispatch();
const count = useSelector(state => state.count);
useCheckTearing();
useRegisterIncrementDispatcher(React.useCallback(() => {
unstable_runWithPriority(unstable_UserBlockingPriority, () => {
dispatch({ type: 'increment' });
});
}, [dispatch]));
const [localCount, localIncrement] = React.useReducer(c => c + 1, 0);
return (
<div>
<button type="button" id="localIncrement" onClick={() => dispatch({ type: 'increment' })}>Increment remote from app</button>
<h1>Remote Count</h1>
{ids.map(id => <Counter key={id} />)}
<div className="count">{count}</div>
<h1>Local Count</h1>
{localCount}
<button type="button" id="localIncrement" onClick={localIncrement}>Increment local count</button>
</div>
);
};
const App = () => (
<Provider store={store}>
<Main />
</Provider>
);
export default App;
so i think that tearing is not a bug, is the consequence of "streaming" updates from a "remote" and react-redux is capable of that with every priority needed. |
so if folks want IMMEDIATE update from everywhere then function useImmediateDispatch() {
const dispatch = useDispatch();
return (...args) => {
unstable_runWithPriority(unstable_UserBlockingPriority, () => {
dispatch(...args);
});
};
} |
removed. |
@salvoravida my demo just demonstrates what I was talking about. The "intentional tearing" caused by Btw, the demo just works because state is actually in React state. If you replace these lines: <pre>
<code>{JSON.stringify(store.getState(), null, 2)}</code>
</pre> with: <pre>
<code>{JSON.stringify(state, null, 2)}</code>
</pre> This is outside of Suspense and .. the broken behavior? or what React would expect the state to look like in there. @markerikson IMHO, from what I read ("Redux is broken") from Dan and what's on React docs, it kind of seems like they just want to say "Lift your state to where it matters instead of having a global store as React has been suggesting for years in their docs" or, let's just say it, ... multiple stores which is actually the reality (you can count all Anyways, I made a couple of codesandbox: https://codesandbox.io/s/test-react-redux-in-concurrent-mode-without-redux-foxxo I think we need a concurrent Redux 🙈 |
In this pr #1455 @dai-shi this fix tearing WITHOUT any priority wrapper even with with "your external buttton" i will do a codesanbox with your demo and my fix. The mental modal is -> store Changes -> subcription call -> enqueque storeState, NOT forceRe-render and read store state on render phase. (that may be discarded or delayed) |
@eddyw throing promise to "comuncate" with Suspense si not releated with tearing, that i have fixed in upside PR. i will investigate more on your demo as soon as possible, with new react-redux. |
here is a temp build if you want to try in CM useSelector Regards! |
@eddyw HERE is your demo "Test Redux in Concurrent mode (with React-Redux)" 🎉 THAT IS 100% working with useTransition !! |
Finally
Enjoy Redux & React-redux 100% CM compatible. Community will save you, little child. Do not worry. |
I guess you already know it, but this "intentional tearing" is different from what I originally talked about, which is tearing in multiple components subscribing separately to a redux store. See this for more information. We need better wording... |
@salvoravida 👍 cool cool! ... but 🙈
wrap within A separate thing:
If not wrapping within |
Issue 3: What would Suspense-oriented React Redux apps look like and how a library should support new best practices@eddyw Wow, nice try. And it raises a question: What is Redux? |
Just throwing random ideas. The above would work dispatching actions outside of React, only once React has initialized. Although, I can see many issues with my implementation (even if done right). For example, if you have two react apps, which is completely valid, the state here wouldn't be able to be shared since only one root instance will own the state.
I honestly don't see how it could break backwards compatibility. This "branching" only happens when you set state within transition. Current apps just don't use transition. Now, if you move your app to CM mode, then start using transition and suspense with React Redux, that's migration and refactoring. If you mean that current apps using RR, should work the same way in CM as they worked before even when using transition, then that's a wrong assumption. In CM, you can't avoid branching of state if you're using transition, I mean .. just don't randomly wrap everything is startTransition. RR uses React state internally to keep selectors, for instance, so if a dispatched action is within a transition, there will always be branching in the selectors that update because of that dispatched action. The issue is, the components that will see branching outside of Suspense will just bail out of re-rendering, if the component is force-rendered, then it'll display the latest value. So, if we just fix tearing and keep everything else as is, this would be the "best practice" rule: This is what "support 80% of CM features with RR" would mean: However, in practice this could be difficult to follow in big React apps. What I got so far is that RR could possibly do little to have full CM support. The last "20% missing support" is branching on Redux itself which probably shouldn't be RR responsibility? Since we don't have an "Async Redux", it's difficult to know if RR could help any further. Idk, I'll just keep on experimenting, it's a nice learning experience lol |
@eddyw i think we cant yet understanding how CM branches merge/swap we have to wait for stable version |
@eddyw Thanks for the summary. Totally agreed. Issue 1: Existing React Redux apps that run in SyncMode should run in Concurrent Mode (createRoot)Issue 2: Existing React Redux apps to get benefit of concurrent rendering (priority)If we don't use Issue 3: What would Suspense-oriented React Redux apps look like and how a library should support new best practicesIf we useTransition, that requires some rules or limited behaviors. That's probably fine. What about middleware? You said it's a different story, but can we set a small set of rules to useTransition properly with middleware? I mean, it's OK to set rules for RR, but we would need to set rules for Redux too for branching. (So, I should have said backward compatibility with useTransition for the entire redux eco system, which might be a wrong assumption.) But, that's still a tiny issue. Honestly, if I were to use |
🎉 habemus papam ! 🎉 |
Brian Vaughn just posted the RFC for the |
While it's quiet here, here's my implementation of react-redux basic hooks with the currently proposed const StoreContext = createContext();
const subscribe = (store, callback) => store.subscribe(callback);
const Provider = ({ store, children }) => {
const contextValue = useMemo(() => ({
source: createMutableSource(store, () => store.getState()),
dispatch: store.dispatch,
}), [store]);
return (
<StoreContext.Provider value={contextValue}>
{children}
</StoreContext.Provider>
);
};
const useDispatch = () => useContext(StoreContext).dispatch;
const useSelector = (selector) => {
const selectorRef = useRef(selector);
useLayoutEffect(() => {
selectorRef.current = selector;
});
const getSnapshot = useCallback((store) => selectorRef.current(store.getState()), []);
const { source } = useContext(StoreContext);
return useMutableSource(source, getSnapshot, subscribe);
}; I'm experimenting it with my tool. |
@dai-shi : yeah, I think my immediate questions would be:
|
According to my observation, it improves but not completely like described in the gist.
I've done a preliminary test with use-context-selector: dai-shi/use-context-selector#12 (comment)
Depends on what you mean by Suspense handling. State branching with useTransition would never be possible with an external store like Redux. Other than that, it should already be working as is. |
|
|
I don't have time to play with this myself atm, but I'd be very interested to see a PR that tries to convert Brian apparently put together an over-simplified example of how we might use it: |
reactjs/rfcs#147 (comment) |
I've been making some progress with |
@nickmccurdy Just a guess, but please try That worked for me: https://github.com/reduxjs/react-redux/pull/1536/files#diff-8a4f74cb696e74165240c9af9cb0ce45R188 |
So, I re-implemented my reactive-react-redux with useMutableSource. In terms of implementation for react-redux v7, looking at my snippet again, I think this one still suffers from the race condition issue #1536. |
Nice. Does this still support multiple store contexts like React Redux? |
Yes, I mean r-r-r doesn't care how developers use contexts with it. |
A simplistic implementation of connect using const requiresOwnProps = mapStateOrMapDispatch => {
return (
typeof mapStateOrMapDispatch === 'function' &&
mapStateOrMapDispatch.length !== 1
)
}
const applyMapStateToProps = (mapStateToProps, state, props) => {
if (typeof mapStateToProps === 'function') {
return requiresOwnProps(mapStateToProps)
? mapStateToProps(state, props)
: mapStateToProps(state)
}
return EMPTY_OBJECT
}
const applyMapDispatchToProps = (mapDispatchToProps, dispatch, props) => {
if (typeof mapDispatchToProps === 'function') {
return requiresOwnProps(mapDispatchToProps)
? mapDispatchToProps(dispatch, props)
: mapDispatchToProps(dispatch)
}
if (mapDispatchToProps && typeof mapDispatchToProps === 'object') {
return bindActionCreators(mapDispatchToProps, dispatch)
}
return { dispatch }
}
const defaultMergeProps = (stateProps, dispatchProps, ownProps) => {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
const useStableObject = obj => {
const objRef = useRef(obj)
useLayoutEffect(() => {
objRef.current = obj
}, [obj])
return shallowEqual(obj, objRef.current) ? objRef.current : obj
}
const useStateProps = (mapStateToProps, ownProps) => {
const { source } = useContext(ReactReduxContext)
const propsForMapState = requiresOwnProps(mapStateToProps) ? ownProps : null
const getSnapshot = useCallback(
store =>
applyMapStateToProps(mapStateToProps, store.getState(), propsForMapState),
[mapStateToProps, propsForMapState]
)
const stateProps = useMutableSource(source, getSnapshot, subscribe)
return useStableObject(stateProps)
}
const useDispatchProps = (mapDispatchToProps, ownProps) => {
const { dispatch } = useContext(ReactReduxContext)
const propsForMapDispatch = requiresOwnProps(mapDispatchToProps)
? ownProps
: null
return useMemo(
() =>
applyMapDispatchToProps(
mapDispatchToProps,
dispatch,
propsForMapDispatch
),
[mapDispatchToProps, dispatch, propsForMapDispatch]
)
}
const useMergeProps = (mergeProps, stateProps, dispatchProps, ownProps) => {
return useMemo(() => mergeProps(stateProps, dispatchProps, ownProps), [
mergeProps,
stateProps,
dispatchProps,
ownProps
])
}
const connect = (
mapStateToProps = null,
mapDispatchToProps = null,
mergeProps = defaultMergeProps
) => WrappedComponent => {
const MemoizedWrappedComponent = React.memo(WrappedComponent)
if (mapStateToProps) {
return React.memo(props => {
const ownProps = useStableObject(props)
const stateProps = useStateProps(mapStateToProps, ownProps)
const dispatchProps = useDispatchProps(mapDispatchToProps, ownProps)
const mergedProps = useMergeProps(
mergeProps,
stateProps,
dispatchProps,
ownProps
)
return <MemoizedWrappedComponent {...mergedProps} />
})
} else {
return React.memo(props => {
const ownProps = useStableObject(props)
const dispatchProps = useDispatchProps(mapDispatchToProps, ownProps)
const mergedProps = useMergeProps(
mergeProps,
EMPTY_OBJECT,
dispatchProps,
ownProps
)
return <MemoizedWrappedComponent {...mergedProps} />
})
}
} |
The React team has announced plans for React 18, per https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html , and created a "React 18 Working Group" discussion forum at https://github.com/reactwg/react-18/discussions . I just filed an issue asking for people to try out React-Redux v7 and the React 18 alphas together, and let us know what sorts of issues pop up. We'd love to have folks help out with this! See #1732 - Investigation: try React-Redux v7 with React 18 alphas for details. |
Sharing my experience related with uMS in React 18 alpha, which is I noticed while experimenting "tearing" with redux with uMS, wrapping a selector with const getSnapshot = (s) => selectCount(s.getState()); causes infinite loop like behavior. In reactive-react-redux, I added a note "selector has to be stable. Either define it outside render or use useCallback if selector uses props." In zustand, I added a note "It is generally recommended to memoize selectors with useCallback.", but this is going to be a requirement with breaking change. |
Hmm. That could be... annoying :) It's so very common to just do |
Do you want to request a feature or report a bug?
Feature
What is the expected behavior?
Full compatibility with Concurrent Mode
Details
We should be close to a Concurrent Mode release, and people are already experimenting with it on their projects. I am not sure but it seems react-redux is not compatible yet, based on this tweet and this pr.
The text was updated successfully, but these errors were encountered: