-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Provide more ways to bail out inside Hooks #14110
Comments
cc @markerikson you probably want to subscribe to this one |
Yay! Thanks :) |
|
I guess you're right the context one is an existing limitation (ignoring the unstable part). |
@alexeyraspopov : nope! Here's an example: function ContextUsingComponent() {
// Subscribes to _any_ update of the context value object
const {largeData} = useContext(MyContext);
// This value may or may not have actually changed
const derivedData = deriveSomeData(largeData);
// If it _didn't_ change, we'd like to bail out, but too late - we're rendering anyway!
}
As an example, assuming we had some magic usage of Imagine our Redux state tree looks like However, while the component is interested in changes to bit 17, it still may not want to re-render - it all depends on whether the derivation has changed. More realistic example: a user list item is interested in changes to |
Perhaps a possible api would be: function Blah() {
// useContext(context, selectorFn);
const val = useContext(Con, c => c.a.nested.value.down.deep);
} And |
@snikobonyadrad: won't work - the second argument is already the export function useContext<T>(
Context: ReactContext<T>,
observedBits: number | boolean | void,
) @gaearon : semi-stupid question. Given that returning the exact same elements by reference already skips re-rendering children, would function ContextUsingComponent() {
const {largeData} = useContext(MyContext);
const derivedData = deriveSomeData(largeData);
const children = useMemo(() => {
return <div>{derivedData.someText}</div>
}, [derivedData]);
} |
@markerikson Yes, but that means that ContextUsingComponent needs to know about this, even if you might otherwise want to put the two useContext+derive calls into a custom Hook. |
Yeah, I know, just tossing it out there as a sort of semi-stopgap idea. Any initial thoughts to what a real API like this might look like? |
Crazy idea: add Not sure how that would mesh with this interrupting further hooks from running, and there is already a problem with the reconciler throwing out hooks that did already run before a component suspends. |
I personally don't like Also, it should be a call: I also thought about the possibility to return early in the render method with a marker ( |
Hi, |
I would enjoy something like this to avoid unnecessary re-renders: const { data } = useContext(MyContext, result => [result.data]) where the second parameter would work like the second parameter of
|
Hi, I would also like the same api as @brunolemos describes, for using it with tools like Unstated which I use as a replacement for Redux store with a similar connect() hoc currently. But I think there is ambiguity in your API proposal @brunolemos not sure exactly what happens if you return [result.data, result.data2] for example. If you return an array you should probably assign an array to the useContext result too? Not sure exactly how What if we could do |
@markerikson how official is this second argument? I see that it is not documented publicly yet. I mention this because the api proposal mentioned by @sam-rad in this comment is what I was expecting it to be eventually, to solve the partial subscribing to context. |
@slorber : See these links for more info on
@gnapse : The React team has said a couple times that they're not sure they want to keep around the |
Looks like useMemo can skip rendering wrapped in it, and the final update(by v-dom diff), but can't skip render function itself. Personally, I agree with using second argument as a selector with shallowEqual, since observedBits is a less common use case and selector can do what observedBits can do. function createSelectorWithProps(props) {
return state => [state.map[props._id]];
}
function useContextWithProps(props) {
return useContext(MyContext, createSelectorWithProps(props));
}
function ContextUsingComponent(props) {
const [item] = useContextWithProps(props);
// return ...........
} But how to handle using multiple context? function ContextUsingComponent(props) {
const [item] = useContextsWithProps(props, context1, context2, context3);
// return ...........
} Finally the problem focuses on [rerender after calculate the data]. |
I just found out about Besides, I find it rather awkward that I would need to kinda declare up front what my consumers might be interested in. What if there is a really an object (a.k.a store) with many properties and I would need each property to assign a bit number and most likely export some kind of map so a consumer can put a final bitmask together. Well, in the end since I am a happy user of MobX, I don't care about this issue that much. Having an observable in the context, I can optimize rendering based on changes in that observable without any extra hassle of comparing stuff or having specific selectors. React won't probably introduce such a concept, but it could be one of the recommendations. |
how about this // assuming createStore return a observable with some handler function or dispatch
function createMyContext() {
const Context = React.createContext();
function MyProvider({ children }) {
const [store, setState] = useState(() => createStore());
return <Context.Provider value={store}>{children}</Context.Provider>
}
return {
Provider: MyProvider,
Consumer: Context.Consumer,
Context,
}
}
const context1 = createMyContext();
const context2 = createMyContext();
function calculateData(store1, store2) {
//return something
}
function ContextUsingComponent() {
const store1 = useContext(context1.Context);
const store2 = useContext(context2.Context);
const [calculated, setCalculated] = useState(() => calculateData(store1, store2));
function handleChange() {
const next = calculateData(store1, store2);
if (!shallowEqual(next, calculated)) {
setCalculated(next);
}
}
useEffect(() => {
const sub1 = store1.subscribe(handleChange);
const sub2 = store2.subscribe(handleChange);
return () => {
sub1.unsubscribe();
sub2.unsubscribe();
}
}, [store1, store2])
// use calculated to render something.
// use store1.dispatch/store1.doSomething to update
} |
Without React NOT providing an option to cancel updates especially from context changes and because using useReducer hook causes various design constraints, we have to resort to good old Redux. Wrote an article based on developing a Redux clone based on existing API - context and hooks which explains more. Two things are clear.
|
@vijayst
This is exactly what react-redux do. And I wrote a example above to implement a react-redux like mechanism. |
The way im doing: Connect
Select
Pure Functional Component with React.memo
Usage
ConclusionThe select function get computed every time the context update, but since it does nothing, its cheap, all the logic must go inside the pure component, that does not get re-render. |
Hi, I'd like to share my workaround. Pseudo codeconst calculateChangedBits = () => 0;
const MyContext = React.createContext(null, calculateChangedBits);
const useMyContext = () => {
const value = React.useContext(MyContext);
const forceUpdate = useForceUpdate();
// and use subscription to detect changes and force update.
}; Concrete examples(edit) Just noticed it's already suggested half a year ago. #14110 (comment)
(edit2) Here's a more simplified library. use-context-selector |
For anyone discovering this Problem
Solutions
Current status
|
For posterity, we ended up going with @dai-shi's ❌ BeforeAll components re-render for any context property change: interface MyContext {
items: Item[]
selectedIndex: number
pagination: PaginationStuff
sorting: SortingStuff
filtering: FilteringStuff
searching: SearchingStuff
}
const ComponentA = () => {
const { items, selectedIndex } = useContext(MyContext)
return // ...etc
}
const ComponentB = () => {
const { filtering, searching } = useContext(MyContext)
return // ...etc
}
const ComponentC = () => {
const { pagination, sorting } = useContext(MyContext)
return // ...etc
} ✅ AfterEach component only re-renders for properties it cares about: const ComponentA = () => {
const items = useContextSelector(MyContext, (v) => v.items)
const selectedIndex = useContextSelector(MyContext, (v) => v.selectedIndex)
return // ...etc
}
const ComponentB = () => {
const filtering = useContextSelector(MyContext, (v) => v.filtering)
const searching = useContextSelector(MyContext, (v) => v.searching)
return // ...etc
}
const ComponentC = () => {
const pagination = useContextSelector(MyContext, (v) => v.pagination)
const sorting = useContextSelector(MyContext, (v) => v.sorting)
return // ...etc
} |
I haven't read ALL the discussion but |
@priolo that's one of the three original mitigation options (#15156 (comment)), which are linked to in the first comment of this thread: #14110 (comment) |
@alexburner you're right, I'm sorry |
There's a few separate issues but I wanted to file an issue to track them in general:
useState
doesn't offer a way to bail out of rendering once an update is being processed. This gets a bit weird because we actually process updates during the rendering phase. So we're already rendering. But we could offer a way to bail on children. Edit: we now do bail out on rendering children if the next state is identical.useContext
doesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering. Edit: see Preventing rerenders with React.memo and useContext hook. #15156 (comment) for solutions to this.The text was updated successfully, but these errors were encountered: