Movatterモバイル変換


[0]ホーム

URL:


Mark's Dev Blog

Random musings on React, Redux, and more, by Redux maintainer Mark "acemarke" Erikson
 Sponsor @markerikson
 Home

Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance

Posted on
#react#redux#reselect#performance#optimization#greatest-hits

This is a post in theIdiomatic Redux series.


An overview of why and how to use Reselect with React and Redux

Intro 🔗︎

In a good Redux architecture, you are encouraged tokeep your store state minimal, and derive data from the state as needed. As part of that process, we recommend that you use "selector functions" in your application, and use the Reselect library to help create those selectors. Here's a deeper look at why this is a good idea, and how to correctly use Reselect.

Update, 2022-03: I've written an updated and more comprehensive version of this for as a Redux docs page:

Using Redux: Deriving Data With Selectors

Please see that article for updated guidance on selector concepts, using Reselect, and using selectors with React-Redux

Basics of Selectors 🔗︎

A "selector function" is simply any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state. Selectors don't have to be written using a special library, and it doesn't matter whether you write them as arrow functions or thefunction keyword. For example, these are all selectors:

const selectEntities = (state) => state.entities;function selectItemIds(state) {  return state.items.map((item) => item.id);}const selectSomeSpecificField = (state) => state.some.deeply.nested.field;function selectItemsWhoseNamesStartWith(items, namePrefix) {  const filteredItems = items.filter((item) =>    item.name.startsWith(namePrefix)  );  return filteredItems;}

You can call your selector functions whatever you want, but it's common to prefix them withselect orget, or end the name withSelector, likeselectFoo,getFoo, orfooSelector (seethis Twitter poll on naming selectors for discussion).

The first reason to use selector functions is for encapsulation and reusability. Let's say that one of yourmapState functions looks like this:

const mapState = (state) => {  const data = state.some.deeply.nested.field;  return { data };};

That's a totally legal statement. But, imagine that you've got several components that need to access that field. What happens if you need to make a change to where that piece of state lives? You would now have to go changeeverymapState function that references that value. So, in the same way thatwe recommend using action creators to encapsulate details of creating actions, we recommend using selectors to encapsulate the knowledge of where a given piece of state lives.Ideally, only your reducer functions and selectors should know the exact state structure, so if you change where some state lives, you would only need to update those two pieces of logic.

One common description of selectors is that they're like "queries into your state". You don't care about exactly how the query came up with the data you needed, just that you asked for the data and got back a result.

Reselect Usage and Memoization 🔗︎

The next reason to use selectors is to improve performance. Performance optimization generally involves doing work faster, or finding ways to do less work. For a React-Redux app, selectors can help us do less work in a couple different ways.

Let's imagine that we have a component that requires a very expensive filtering/sorting/transformation step for the data it needs. To start with, itsmapState function looks like this:

const mapState = (state) => {  const { someData } = state;  const filteredData = expensiveFiltering(someData);  const sortedData = expensiveSorting(filteredData);  const transformedData = expensiveTransformation(sortedData);  return { data: transformedData };};

Right now, that expensive logic will re-run forevery dispatched action that results in a state update, even if the store state that was changed was in a part of the state tree that this component doesn't care about.

What we really want is to only re-run these expensive steps ifstate.someData has actually changed. This is where the idea of "memoization" comes in.

Memoization is a form of caching. It involves tracking inputs to a function, and storing the inputs and the results for later reference. If a function is called with the same inputs as before, the function can skip doing the actual work, and return the same result it generated the last time it received those input values.

TheReselect library provides a way to create memoized selector functions. Reselect'screateSelector function accepts one or more "input selector" functions, and an "output selector" function, and returns a new selector function for you to use.

createSelector can accept multiple input selectors, which can be provided as separate arguments or as an array. The results from all the input selectors are provided as separate arguments to the output selector:

const selectA = (state) => state.a;const selectB = (state) => state.b;const selectC = (state) => state.c;const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {  // do something with a, b, and c, and return a result  return a + b + c;});// Call the selector function and get a resultconst abc = selectABC(state);// could also be written as separate arguments, and works exactly the sameconst selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {  // do something with a, b, and c, and return a result  return a + b + c;});

When you call the selector, Reselect will run your input selectors with all of the arguments you gave, and looks at the returned values. If any of the results are=== different than before, it will re-run the output selector, and pass in those results as the arguments. If all of the results are the same as the last time, it will skip re-running the output selector, and just return the cached final result from before.

In typical Reselect usage, you write your top-level "input selectors" as plain functions, and usecreateSelector to create memoized selectors that look up nested values:

const state = {  a: {    first: 5,  },  b: 10,};const selectA = (state) => state.a;const selectB = (state) => state.b;const selectA1 = createSelector([selectA], (a) => a.first);const selectResult = createSelector([selectA1, selectB], (a1, b) => {  console.log('Output selector running');  return a1 + b;});const result = selectResult(state);// Log: "Output selector running"console.log(result);// 15const secondResult = selectResult(state);// No log outputconsole.log(secondResult);// 15

Note that the second time we calledselectResult, the "output selector" didn't execute. Because the results ofselectA1 andselectB were the same as the first call,selectResult was able to return the memoized result from the first call.

It's important to note that by default, Reselect only memoizes the most recent set of parameters. That means that if you call a selector repeatedly with different inputs, it will still return a result, but it will have to keep re-running the output selector to produce the result:

const a = someSelector(state, 1); // first call, not memoizedconst b = someSelector(state, 1); // same inputs, memoizedconst c = someSelector(state, 2); // different inputs, not memoizedconst d = someSelector(state, 1); // different inputs from last time, not memoized

Also, you can pass multiple arguments into a selector. Reselect will call all of the input selectors with those exact inputs:

const selectItems = (state) => state.items;const selectItemId = (state, itemId) => itemId;const selectItemById = createSelector(  [selectItems, selectItemId],  (items, itemId) => items[itemId]);const item = selectItemById(state, 42);/*Internally, Reselect does something like this:const firstArg = selectItems(state, 42);  const secondArg = selectItemId(state, 42);    const result = outputSelector(firstArg, secondArg);  return result;  */

Because of this, it's important that all of the "input selectors" you provide should accept the same types of parameters. Otherwise, the selectors will break.

const selectItems = state => state.items;// expects a number as the second argumentconst selectItemId = (state, itemId) => itemId;// expects an object as the second argumentconst selectOtherField (state, someObject) => someObject.someField;const selectItemById = createSelector(    [selectItems, selectItemId, selectOtherField],    (items, itemId, someField) => items[itemId]);

In this example,selectItemId expects that its second argument will be some simple value, whileselectOtherField expects that the second argument is an object. If you callselectItemById(state, 42),selectOtherField will break because it's trying to access42.someField.

You can (and probablyshould) use selector functionsanywhere in your application that you access the state tree. That includesmapState functions, thunks, sagas, observables, middleware, and even reducers.

Selector functions are frequently co-located with reducers, since they both know about the state shape. However, it's up to you where you put your selector functions and how you organize them.

Optimizing Performance With Reselect 🔗︎

Let's go back to the "expensivemapState" example from earlier. We really want to only execute that expensive logic whenstate.someData has changed. Putting the logic inside a memoized selector will do that.

const selectSomeData = (state) => state.someData;const selectFilteredSortedTransformedData = createSelector(  selectSomeData,  (someData) => {    const filteredData = expensiveFiltering(someData);    const sortedData = expensiveSorting(filteredData);    const transformedData = expensiveTransformation(sortedData);    return transformedData;  });const mapState = (state) => {  const transformedData = selectFilteredSortedTransformedData(state);  return { data: transformedData };};

This is a big performance improvement, for two reasons.

First, now the expensive transformation only occurs ifstate.someData is different. That means if we dispatch an action that updatesstate.somethingElse, we won't do any real work in thismapState function.

Second, the React-Reduxconnect function determines if your real component should re-render based on the contents of the objects you return frommapState, using "shallow equality" comparisons. If any of the fields returned are=== different than the last time, thenconnect will re-render your component. That means that you should avoid creating new references in amapState function unless needed. Array functions likeconcat(),map(), andfilter() always return new array references, and so does the object spread operator. By using memoized selectors, we can return the same references if the data hasn't changed, and thus skip re-rendering the real component.

Advanced Optimizations with React-Redux 🔗︎

There's a specific performance issue that can occur when you use memoized selectors with a component that can be rendered multiple times.

Let's say that we have this component definition:

const mapState = (state, ownProps) => {    const item = selectItemForThisComponent(state, ownProps.itemId);    return {item};}const SomeComponent = (props) => <div>Name: {props.item.name}</div>;export default connect(mapState)(SomeComponent);// later<SomeComponent itemId={1} /><SomeComponent itemId={2} />

In this example,SomeComponent is passingownProps.itemId as a parameter to the selector. When we render multiple instances of<SomeComponent>, each of those instances are sharing the same instance of theselectItemForThisComponent function. That means that when an action is dispatched, each separate instance of<SomeComponent> will separately call the function, like:

// first instanceselectItemForThisComponent(state, 1);// second instanceselectItemForThisComponent(state, 2);

As described earlier, Reselect only memoizes on the most recent inputs (ie, it has a cache size of 1). That means thatselectItemForThisComponent willnever memoize correctly, because it's never being called with the same inputs back-to-back.

This code will still run and work, but it's not fully optimized. For the absolute best performance, we need a separate copy ofselectItemForThisComponent for each instance of<SomeComponent>.

The React-Reduxconnect function supports a special "factory function" syntax formapState andmapDispatch functions, which can be used to create unique instances of selector functions for each component instance.

If the first call to amapState ormapDispatch function returns a function instead of an object,connect will use that returned function as therealmapState ormapDispatch function. This gives you the ability to create component-instance-specific selectors inside the closure:

const makeUniqueSelectorInstance = () =>  createSelector([selectItems, selectItemId], (items, itemId) => items[itemId]);const makeMapState = (state) => {  const selectItemForThisComponent = makeUniqueSelectorInstance();  return function realMapState(state, ownProps) {    const item = selectItemForThisComponent(state, ownProps.itemId);    return { item };  };};export default connect(makeMapState)(SomeComponent);

Both component 1 and component 2 will get their own unique copies ofselectItemForThisComponent, and each copy will get called with consistently repeatable inputs, allowing proper memoization.

Final Thoughts 🔗︎

Likeother common Redux usage patterns,you are not required to use selector functions in a Redux app. If you want to write deeply nested state lookups directly in yourmapState functions or thunks, you can. Similarly, you don'thave to use the Reselect library to create selectors - you can just write plain functions if you want.

Having said that,you are encouraged to use selector functions, and to use the Reselect library for memoized selectors. There's also many other options for creating selectors, including using functional programming utility libraries like lodash/fp and Ramda, and other alternatives to Reselect. There's alsoutility libraries that build on Reselect to handle specific use cases.

Further Information 🔗︎


This is a post in theIdiomatic Redux series.Other posts in this series:

Recent Posts

Presentations: The State of React and the Community in 2025The State of React and the Community in 2025Presentations: Maintaining a Library and a CommunityReact Advanced 2024: Designing Effective DocumentationReact Summit 2024: Why Use Redux Today?

Top Tags

63 redux56 javascript49 react31 presentation30 greatest-hits

Greatest Hits

Greatest Hits: The Most Popular and Most Useful Posts I've WrittenRedux - Not Dead Yet!Why React Context is Not a "State Management" Tool (and Doesn't Replace Redux)A (Mostly) Complete Guide to React Rendering BehaviorPresentations: Modern Redux with Redux ToolkitWhen (and when not) to reach for ReduxThe Tao of Redux, Part 1 - Implementation and IntentThe History and Implementation of React-ReduxThoughts on React Hooks, Redux, and Separation of ConcernsReact Boston 2019: Hooks HOCs, and TradeoffsUsing Git for Version Control Effectively

Series

21 Blogged Answers4 Codebase Conversion4 Coding Career Advice2 Declaratively Rendering Earth In 3d5 How Web Apps Work8 Idiomatic Redux6 Newsletter13 Practical Redux32 Presentations5 Site Administrivia

[8]ページ先頭

©2009-2025 Movatter.jp