Tutorial

Performant Redux Selectors with Reselect

Published on July 15, 2018
    author

    Alligator.io

    Performant Redux Selectors with Reselect

    In Redux a selector is a piece of logic that gets a specific piece of state from the store. Additionally, a selector can compute data from a given state, allowing the store to keep only basic raw data. Selectors are usually used as part of the binding between the store and the container components. In React for example, the mapStateToProps function that’s passed to React Redux’s connect function is where selectors would be used to select the needed slices of state for a container component.

    Selectors can be created without the help of a library like Reselect, but using Reselect carries a few benefits in terms of developer experience and performance:

    • Selectors created using Reselect’s createSelector function are memoized. That’s a fancy word to mean that the function remembers the arguments passed-in the last time it was invoked and doesn’t recalculate if the arguments are the same. You can view it a little bit like caching.
    • Reselect selectors can be composed/chained together easily. This way, each selector stays small and focused on one task.

    Installation

    Just add the reselect package to your project using npm or Yarn:

    $ npm install reselect
    
    # or, using Yarn:
    $ yarn add reselect
    

    With this, you can start creating selectors either in the same file as your reducers, or in their own separate file. Here for example we create some selectors for a simplistic todo app where we’ll like to select todo items that contain the string *“milk”* and those that also contain the string *“bread”*:

    todo.reducer.js (partial)
    // ...
    import { createSelector } from 'reselect';
    
    const todoSelector = state => state.todo.todo;
    
    export const todosWithMilk = createSelector([todoSelector], todos => {
      return todos.filter(todo => todo.title.toLowerCase().includes('milk'));
    });
    
    export const todosWithMilkAndBread = createSelector([todosWithMilk], todos => {
      return todos.filter(todo => todo.title.toLowerCase().includes('bread'));
    });
    
    // ...
    

    With this example, we have 3 selectors: todoSelector, todosWithMilk and todosWithMilkAndBread. The first selector is known as an input selector. Input selectors are as simple as it gets, they take in the state as an argument and return a slice of that state. Input selectors should always be pure functions.

    The 2nd and 3rd selectors in our example, todosWithMilk and todosWithMilkAndBread, are selectors created using Reselect’s createSelector function. createSelector expects 2 arguments: an array of input selector(s) as the 1st argument and a function as the 2nd argument, known as the transform function. The transform function receives the result(s) from the input selector(s) as arguments and should return the desired computed piece of state.

    Selector composition is also demonstrated here, with the todosWithMilkAndBread being composed on top of the todosWithMilk selector. This helps keep your selector logic simple.

    Note that you can also pass-in the input selectors to createSelector as a list of arguments, no need for them to be in an array. As long as the last argument passed-in is the transform function.

    Using our selectors

    Let’s make use of our todosWithMilk and todosWithMilkAndBread selectors in a Todos container component:

    containers/Todos.js
    import React from 'react';
    import { connect } from 'react-redux';
    
    import { deleteTodo } from '../actions';
    import { todosWithMilk, todosWithMilkAndBread } from '../reducers';
    
    function Todos({
      withMilk,
      withMilkAndBread,
      error,
      loading
    }) {
      return (
        <div>
          {/* ... */}
        </div>
      );
    }
    
    const mapStateToProps = state => {
      return {
        withMilk: todosWithMilk(state),
        withMilkAndBread: todosWithMilkAndBread(state),
        error: state.todo.error,
        loading: state.todo.loading
      };
    };
    
    const mapDispatchToProps = dispatch => {
      return {
        onDelete: id => {
          dispatch(deleteTodo(id));
        }
      };
    };
    
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(Todos);
    

    As you can see, it’s as easy as calling our selectors with the state object passed-in. Behind the scenes our selectors will verify if it was called with the same argument values the last time it ran, and it won’t recompute the state if that’s the case.

    More Realistic Example

    Our previous example is rather simplistic and in a real app you’ll instead most often have multiple input selectors and compute the desired slice of state based on the result of these input selectors. For example, in a more realistic todo app, you could have an input field to filter down the visible todo items that contain a specified search term. This could be accomplished with selectors that look a little bit like the following:

    todo.reducer.js (partial)
    // ...
    import { createSelector } from 'reselect';
    
    const todoSelector = state => state.todo.todos;
    const searchTermSelector = state => state.todo.searchTerm;
    
    export const filteredTodos = createSelector(
      [todoSelector, searchTermSelector],
      (todos, searchTerm) => {
        return todos.filter(todo => todo.title.match(new RegExp(searchTerm, 'i')));
      }
    );
    
    // ...
    

    And with this, we can use the filteredTodos selectors to get all the todos if there’s no searchTerm set in the state, or a filtered list otherwise.

    👷‍ I hope that with this you’ll feel comfortable to start using selectors as part of your Redux apps and reap the performance benefits. You can refer to the project’s readme for a more in-depth look at the API and to learn about more advanced use cases like accessing component props in selectors.

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Alligator.io

    author

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.