Search code examples
javascriptreactjsreact-reduxcomparisonequality

Strict Equality (===) versus Shallow Equality Checks in React-Redux


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)


Solution

  • 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