I'm studying how Hooks APIs provided by React-Redux-v.7.1 works, and saw it mentioned in Equality Comparisons and Updates (https://react-redux.js.org/api/hooks#equality-comparisons-and-updates) that:
"As of v7.1.0-alpha.5, the default comparison is a strict === reference comparison. This is different than connect(), which uses shallow equality checks on the results of mapState calls to determine if re-rendering is needed. This has several implications on how you should use useSelector()."
I'm wondering why strict equality is better than shallow equality that used by connect()? Then I looked into their equality comparisons:
The default strict equality check of useSelector() is simply checking a===b
const refEquality = (a, b) => a === b
And the equality checks in react-redux/src/connect/connect.js is using Object.is() and also other checks, which is same as that in react.
// The polyfill of Object.is()
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
return true
}
According to the description of Object.is() in MDN: "Object.is does no type conversion and no special handling for NaN, -0, and +0 (giving it the same behavior as === except on those special numeric values)."
I have no idea why a === b is better than a series of equality checks. (It's my first time asking questions here, apologize for rudeness or lack of information)
React-Redux uses shallow equality checking to determine whether the component it's wrapping needs to be re-rendered. To do this, it assumes that the wrapped component is pure; that is, that the component will produce the same results given the same props and state.
shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects. It does this by iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal.
Shallow compare works by checking if two values are equal in case of primitive types like string, numbers and in case of object it just checks the reference. So if you shallow compare a deep nested object it will just check the reference not the values inside that object.
What you call "shallow equal" is identity: two references (i.e. objects) are identical if they are the very same instances. If you know what pointers are in other languages, you can compare identity to pointer equality. What you call "deep equal" is equality: two objects a and b are equal if a.
With connect
, mapStateToProps
returns a composite object of all of the state being selected from the store, so a shallow comparison across its keys makes sense. With useSelector
, the pattern is often to only return a single value for each invocation of useSelector
, similar to the way that the useState
hook only handles a single value instead of all of the state values. So if each call to useSelector
returns a value directly then a strict equality check makes sense vs a shallow comparison. A short example should make this more clear.
import {connect} from 'react-redux';
const mapStateToProps = state => (
{keyA: state.reducerA.keyA, keyB: state.reducerB.keyB}
);
export default connect(mapStateToProps)(MyComponent);
Here mapStateToProps
is called every time the store changes in any way, so the return value will always be a new object, even if keyA
and keyB
do not change. So a shallow comparison is used to decide if a re-render is needed.
For the hooks case:
import {useSelector} from 'react-redux';
function MyComponent(props) {
const keyA = useSelector(state => state.reducerA.keyA);
const keyB = useSelector(sate => state.reducerB.keyB);
...
}
Now the result of the useSelector
hooks are the individual values from the store, not a composite object. So using strict equality as the default here makes sense.
If you want to use only a single useSelector
hook that returns a composite object, the docs you linked to have an example of using the shallowEqual
equality function: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With