Search code examples
reactjsreduxreselect

Reselect selector keeps recomputing with the same input


I don't understand why reselect library re-calculates selector with the same input. I checked FAQ, but it doesn't answer my question.

Here's an example with react, redux, reselect

Codesandbox

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";
import { createSelector } from "reselect";

const initialState = {
  counter: 1
};

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { ...state, counter: state.counter + 1 };
  }
  if (action.type === "DECREMENT") {
    return { ...state, counter: state.counter - 1 };
  }
  return state;
};

const store = createStore(reducer);

class ChildComp extends React.Component {
  handleIncrement = () => {
    this.props.increment();
  };

  handleDecrement = () => {
    this.props.decrement();
  };

  render() {
    const { counter, isEven } = this.props;
    return (
      <div>
        <div>counter: {counter}</div>
        <div>is even: {JSON.stringify(isEven)}</div>
        <button onClick={this.handleIncrement}>increment</button>
        <button onClick={this.handleDecrement}>decrement</button>
      </div>
    );
  }
}

const getCounter = state => state.counter;
const isEvenSelector = createSelector(
  getCounter,
  counter => {
    console.log("calculate is even for", counter);
    return counter % 2 === 0;
  }
);

const mapStateToProps = state => {
  return {
    counter: state.counter,
    isEven: isEvenSelector(state)
  };
};

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({ type: "INCREMENT" }),
    decrement: () => dispatch({ type: "DECREMENT" })
  };
};

const Child = connect(
  mapStateToProps,
  mapDispatchToProps
)(ChildComp);

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <Child />
      </Provider>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Expected behavior:

initial (counter 1) // calculate is even for 1
increment (counter 2) // calculate is even for 2
decrement (counter 1) // shouldn't console.log, because the result was already calculated

Actual behavior:

initial (counter 1) // calculate is even for 1
increment (counter 2) // calculate is even for 2
decrement (counter 1) // calculate is even for 1


Solution

  • Only the result for the previous set of inputs is cached.

    The docs say:

    Selectors created with createSelector have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.

    So, if the value for counter hadn't changed then you would see no console log. But as it isn't the same as the last time the selector function executed the result has to be calculated again.