-
Notifications
You must be signed in to change notification settings - Fork 61
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
Support for custom equality function #19
Comments
Hi, thanks for opening an issue.
(I was stupid back then..) |
@dai-shi I'm not sure I follow. If a selector composes an object, using referential equality will always trigger a rerender. Shallow equality would help; how would |
|
@lishine The selector callback is cached, but it returns a new object every time. The selected value will always be different referentially. |
Ok, my bad, it actually never triggers with the above code. |
That would violate the rule of hooks, wouldn't it? Using "useMemo" in a selector means that it's called in function that is neither a component nor a custom hook. I suppose the deeper underlying point is that you can memoize the returned object (not through useMemo, but something else like reselect or just rolling your own memoization), but that's just ultimately a roundabout way of doing custom equality checks. |
@victorporof Sorry, I think I mixed the two points. Ref equality is almost default with useMutableSource (and React in general), so supporting equalityFn is an extra work that I think shouldn't be part of this library. So, one would need to use memoization library before passing. I'm not very familiar, but reselect can be used or there would be more alternatives. (Requiring |
To be clear, this is the point myself (and I believe OP) are referring to: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates If this library choses to only support memoization instead of equality checks for the generated selector data, then sure. However, the |
Right, that was my bad.. (It's just a bit related when one were to implement it.) |
This is what I meant. |
@lishine You can't useMemo inside a useCallback, or another function that isn't a hook or a function component. |
@lishine Yeah. Note that |
Sure in render , in the component that includes the provider. |
I write a custom hook that creates the state. That state is passed to the provider. |
1 similar comment
I write a custom hook that creates the state. That state is passed to the provider. |
@lishine Are you talking about memoizing the whole state that is passed to the provider? That is different from from selecting multiple values from that state. |
If you want to return an object from the selector that includes several properties of the state, you create this object beforehand where you manage the state. And the select this property in the selector. |
@lishine Aha, I see what you mean. I think that's a reasonable workaround (just like many others, such as using reselect, or memoizing the returned value from a selector on the spot). I think we're in agreement that being constrained with just being able to use referential equality doesn't mean that there's no other ways of doing this. I'd say that there can be various possible combinations of substate that the provider component would then have to know about, which means propagating these concerns up the component tree. I maintain that this workaround (and memoization inside selectors) are less ergonomic or natural than just being able to pass in a custom equality function in the selector. However I also concede that this might be because of some folks just being used to react-redux's approach. |
Note: doing this sort of memoization inside the provider component doesn't seem possible when it'd depend on various props that are available only to some deeply descendant component. |
In this context, my preference it to use multiple |
@dai-shi Multiple selectors aren't suitable in all circumstances. Consider this:
vs.
...where Multiple selectors won't help. You want to build a memoized selector factory, with a memo'ed caching selector for each component instance. Which is fine (and what you'd do with reselect for example); it's just that shallow equality would be so much simpler, and one of the reasons why react-redux opted to support it in addition to allowing consumers roll up their own memoization strategies, instead of enforcing them. |
Thanks for a concrete example. Totally makes sense. Here's a rough idea. There might be some issues in CM. 🤔 const useSelectorWithEqlFn = (selector, eqlFn) => {
const ref = useRef();
const patchedSelector = useCallback((state) => {
const selected = selector(state);
if (eqlFn(ref.current, selected)) {
return ref.current;
}
ref.current = selected;
return selected;
}, [selector, eqlFn]);
return useSelector(patchedSelector);
}; |
@dai-shi While that works fine, that would trigger a re-render wouldn't it? My point wasn't necessarily about receiving a memoized object for the consumer code, but making sure that the components don't rerender in the first place. |
Scratch that, I think that should work fine. You'd also need to be careful that |
Wow this blew up :) Yes @victorporof gave a reasonable example use case. Mine was pulling from multiple parts of the state, contingent on a null check. Here's a simplified example:
which could be better replaced by
I use typescript and sadly the first version loses the proof that The other obvious workaround is to use reselect and I think it's fair to recommend that instead :) Especially if |
I haven't actually used reselect before - I looked at the documentation, and had some trouble understanding how to apply it to my example. Would it be like this?
I guess that works although it's quite finicky |
Or maybe this works? reselect docs have some room for improvement regarding extra arguments :)
with the caveat that there has to be one |
I hadn't thought about this, but it's very convincing. Thanks for the use case.
It's more like
You want to avoid accessing const fooAndBarSelector = createSelector(
s => s.flag,
(s, { fooId }) => s.flag && s.foos[fooId],
(s, { barId }) => s.flag && s.bars[barId],
(flag, foo, bar) => flag && { foo, bar },
) |
Oh yes, that makes more sense, thank you |
To circle back around, are there any implications of concurrent mode and/or useMutableSource on the notion of introducing a custom equality function parameter? Or, is that a separate discussion, and is the main issue with the custom equality function idea one of API design and steering usage in the right way? |
Besides my preference on API design, Some references: |
Reading those links there doesn't seem to be a place for this custom equality function idea with useMutableSource. But I found the API highly confusing. I was particular confused by the redux example
that doesn't look very ergonomic or similar to how anybody uses redux selectors today... am I missing something here? |
I'm not sure if I understand your point. (and I'm not sure if I responded well to your concern.) BTW, if you are interested in a full implementation with useMutableSource for Redux. I developed this dai-shi/reactive-react-redux#48 . The API is not backward compatible and more like proposing a new pattern. |
Okay, I may need to change my mind if React supports equalityFn. ref: facebook/react#20646 |
I was thinking about this again recently, actually. I have seen more situations since I opened the issue where |
It's still doubtful, if react really supports equalityFn. There's probably going to be a big debate. For now, we should focus on creating a wrapper. import { useContextSelector } from 'use-context-selector';
export const useSelectedContext = (ctx, selector, eqlFn) => {
const patchedSelector = useMemo(() => {
let prevValue = null;
return (state) => {
const nextValue = selector(state);
if (eqlFn(prevValue, nextValuue)) {
return prevValue;
}
return (prevValue = nextValue);
}
}, [ctx, selector, eqlFn]);
return useContextSelector(ctx, patchedSelector);
}; |
patchedSelector, as a hook https://gist.github.com/johnrom/4e8bc65110c689006663c7736539e892 |
patched selector as a package https://www.npmjs.com/package/use-optimized-selector |
Hopefully this wrapper solution or something similar can be considered as the standard export of the package. I was really hoping to replace Redux with Context API for application state management, however while The equality function option would be especially useful when replacing Redux with Context since you usually need to grab both values and methods (mutations) from your context object and so referential equality is a clear problem. With Redux you're doubly covered in this regard, because instead of selecting mutations you dispatch them through a separate mechanism. Having the wrapper function mentioned in the above comment as a work-around is great, but I'm unsure how stable the solution is. |
While I understand where you are coming from, I'm still not confident if the equalityFn is going to be a mainstream in the React core. I'm not sure either how we can make it CM friendly. My hope is to do some experiment against a new experimental build of React and learn how it behaves (but that doesn't ensure anything for the future React versions.) We had some discussions above and some of them are convincing the need of equalityFn. But, if it's about methods or Redux dispatch, I'd suggest to use two hooks. const foo = useContextSelector(v => v.state.foo);
const dispatch = useContextSelector(v => v.dispatch);
It should be just fine at this point. Not sure with concurrent mode thing in the future, but I guess your concern is for the future React too? So, including the workaround in the lib doesn't seem to help, does it? |
Thanks for this nice hook. I observed a problem that I want to share, maybe it will help others too. If you intend to use this hook like this for example: function useSelectedArray(keys: string[]): string[] {
return useSelectedContext(formStateContext,
(state) => keys.map(key => state[key]),
(prev, next) => deepCompare(prev, next))
} The hook will never return the cached value but always a new value. The problem lies in the dependency of the To fix this, I edited the dependency array as follows Another option would probably be to use a |
@timbicker Hi, you understand it correctly. My expectation is to use |
Alright, thanks! |
Here is the typescript version: export const useEqlContextSelector = <T extends any, R extends any>(
ctx: Context<T>,
selector: (val: T) => R,
isEql: (a: R | null, b: R) => boolean
) => {
const patchedSelector = useMemo(() => {
let prevValue: R | null = null;
return (state: T) => {
const nextValue: R = selector(state);
if (prevValue !== null && isEql(prevValue, nextValue)) {
return prevValue;
}
prevValue = nextValue;
return nextValue;
};
}, [isEql, selector]);
return useContextSelector(ctx, patchedSelector);
}; |
I don't think import { useContextSelector } from 'use-context-selector';
export const useSelectedContext = (ctx, selector, eqlFn) => {
const prevValue = useRef(null);
const patchedSelector = (state) => {
const nextValue = selector(state);
if (eqlFn(prevValue.current, nextValuue)) {
return prevValue.current;
}
return (prevValue.current = nextValue);
}
};
return useContextSelector(ctx, patchedSelector);
}; |
Just found that I implemented pretty similar package, but with a bit more features:
|
Yeah, we recently implemented |
Thank you! |
FYI, our latest recommendation is #109 (comment) and if you need custom equality function, import {
createContext as createContextOrig,
useContext as useContextOrig,
useRef,
} from 'react';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
export const createContext = (defaultValue) => {
const context = createContextOrig();
const ProviderOrig = context.Provider;
context.Provider = ({ value, children }) => {
const storeRef = useRef();
let store = storeRef.current;
if (!store) {
const listeners = new Set();
store = {
value,
subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); },
notify: () => listeners.forEach((l) => l()),
}
storeRef.current = store;
}
useEffect(() => {
if (!Object.is(store.value, value)) {
store.value = value;
store.notify();
}
});
return <ProviderOrig value={store}>{children}</ProviderOrig>
};
return context;
}
export const useContextSelector = (context, selector, equalityFn) => {
const store = useContextOrig(context);
return useSyncExternalStoreWithSelector(
store.subscribe,
() => selector(store.value),
undefined,
equalityFn
);
}; With that, let's close this. |
It seems like the library is currently hardcoded to compare the value produced by the selector using
Object.is
use-context-selector/src/index.js
Line 82 in ba7476a
Sometimes it may be useful to use a different equality comparison such as shallow equal when composing an object. The redux
useSelector
API has an extra optional parameter for this purpose, which can be used likeWould you consider supporting this in use-context-selector?
The text was updated successfully, but these errors were encountered: