-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
Effect memoization and immutable data structures #15154
Comments
Do they have some kind of a hash? |
Yes, they do, but ClojureScript doesn't rely on hash equality. It's reliable only as a fast fail check, but doesn't guarantee that two objects with the same hash are structurally equal. |
How deep can the structural comparison go? |
It can be full traversal when comparing literal values, but in practice a new value is often a slightly modified version. Because cljs utilizes structural sharing and comparison performs identity check on every level the operation short-circuits early. |
It is not recommended to go too deep. Eg as far as deep traversal. This would be a bad default. If a data structure wanted to at most compare four levels deep, it could deconstruct eg a trie to the fourth level and pass that level to useEffect. Eg a wrapper around useEffect. |
If I understand this correctly, you are suggesting to hook into equivalence procedure (or implement a separate one) so it can be stopped at arbitrary level in data structure and pass the rest of it as deps into If that's correct, I don't think this is practically more useful than hacking around Clojure's equality semantics are used in every ClojureScript wrapper for React, this was never a problem. On the other hand there's a question about if it's common enough that compound objects are being used as deps in btw, I think the issue touches |
The issue with using "equality equivalence" values for fast comparison is that you still pay the price of constructing those values. I think it also touches From a performance point of view, the question is -- would an equality comparison be more an expensive an unnecessary re-render? In the case of I understand that exposing a custom equality algorithm might expose footguns when used wrongly. At the same time, the immutable data structure libraries (ClojureScript, Immutable.js) do have a very fast and optimised value equality comparison algorithm. As it stands, relying on Thank you for taking the time to discuss this, and also thank you for coming up and documenting the Hooks API so well. It's a game changer! |
|
Ah that's right -- I was thinking of useMemo. |
We greatly appreciate the React team’s time discussing this with us. The way I understand it, equality with ClojureScript’s immutable data basically works like:
Since it uses structural sharing, this can have differing performance depending on whether the two values are derived from a common ancestor or not. The worst case is that they are completely distinct-but-equal structures, in which case it does a depth-first traversal of the whole thing. My general take at this point is that adding a comparator to I can think of three cases right now where I see reference equality causing undesired behavior:
I think that (1) is something that we just have to discourage, or at least educate on the tradeoffs. That is the pathological case where even equality with structural sharing will have to do a deep traversal of the entire structure to be sure every render, which could be quite expensive. With (2), we can take advantage of the fact that state changes are often derived, and also that we can do the equality check on update rather than on every render. Currently I’m thinking of exposing this as a general state hook to help with this: function useImmutableState(initial) {
let [v, u] = useState(initial);
function updater(x) {
if (!isFunction(x)) {
// if x is not a fn, then it’s likely not derived from previous state
// so we don’t bother checking equality
return u(x);
}
// If it is a function, new state will be derived from previous
// so we can take advantage of structural sharing to do fast
// equality here
function update(currentState) {
let newState = x(currentState);
if (immutableEqual(currentState, newState)) {
return currentState; // should bail
} else {
return newState;
}
}
return u(update);
}
return [v, updater];
} I think that this takes care of the case where a user somehow applies a transformation to state that ends up with a structurally similar value. For the case of doing a network request and parsing JSON to EDN or whatever (3), I’m not sure yet what the best way to handle that is; it’s going to be a deep equality no matter what (just like passing in a data literal), but at least it’s not being done each render. Perhaps a custom hook using a ref to do a comparison would guarantee equality, and be worth the cost in performance? // If the value passed in is structurally equal to the one saved in the ref,
// it will return the one saved in the ref to preserve reference equality
function useImmutableValue(value) {
let v = useRef(value);
if (!immutableEquals(value, v.current)) {
v.current = value;
}
return v.current;
} Which could be used something like: function MyComponent(props) {
let result = useImmutableValue(useDataFromAPI(props.params));
useEffect(function doThingBasedOnResult () { ... }, [result]);
...
} In general, I think we’re looking for help / guidance in how best to leverage our immutable data structures with the latest React changes. It’d be nice to know if we’re thinking about this correctly, or if we should take a different tack. |
I think typed-in literals are too small to worry about. From #14476 -
Comparing small structures like |
Note that I've been bitten in the past by such structural comparison in Clojurescript before. Comparing 2 vectors (arrays) of deeply nested maps when only the last element was different (think products list), it mean basically that the comparison checked every field and nested field of every object in the vector to finally find a difference. The performance hit just killed the UI. |
For me it seems that the user of the API is best placed to know: a) How deep/complex the compared values will be |
Exactly. |
Which means that if I want to skip based on a custom equality check (by custom I mean not the one provided by React), I can just do it before calling |
Current design of
useEffect
requires dependencies to be either primitive values or references to the same object, because shallow equality check relies onObject.is
which is an identity check for objects.The above means that there's no way to perform structural comparison, which is needed for immutable data structures when identity check fails.
To maintain backwards compatibility a comparator function could be provided as the third argument to
useEffect
:The goal here is to preserve an ease of use of the API with immutable data structures in order to provide an idiomatic usage of
useEffect
in ClojureScript and other environments that rely on immutability e.g. Immutable.jscc @mhuebert @Lokeh @orestis
The text was updated successfully, but these errors were encountered: