Skip to content

Commit 731daef

Browse files
committed
Docs: overview of selector functions
1 parent 7458311 commit 731daef

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
---
2+
id: selector-functions
3+
title: Selector Functions
4+
hide_title: true
5+
sidebar_label: Selector Functions
6+
---
7+
8+
## Introduction
9+
10+
When you design a redux application, you are encouraged to have your application data in a [normalised state](https://redux.js.org/recipes/structuringreducers/normalizingstateshape#normalizing-nested-data) as well as [keep your store state minimal and derive the appropriate pieces of information when needed](https://redux.js.org/recipes/computingderiveddata). As our application grows different parts of it will require different slices of the state and to grab the correct information we recommend to use _selector functions_ which will help you retrieve only the necessary part of the data that is required by you. In this section, we will have a deeper look into selectors and how to correctly use _reselect_, a selector library, to create your selectors.
11+
12+
## Understanding selectors
13+
14+
A selector is any function that accepts as an argument our application state or part of it and returns the data which is required by our needs. We don’t need to use any library to create a selector and it doesn’t matter if you write them as an arrow function or with the `function` keyword. It is a good practice to keep your selector as a pure function which will also make it easier to test. For example:
15+
16+
```
17+
const selectRecipes = state => state.recipes;
18+
19+
function selectIngredientsIds(state) {
20+
return state.ingredients.map(ingredient => ingredient.id);
21+
}
22+
23+
const selectSomeSpecificField = state => state.some.deeply.nested.field;
24+
25+
function selectRestaurantsInCity(restaurants, city) {
26+
const filteredRestaurants = restaurants.filter(restaurant => restaurant.city === city);
27+
return filteredRestaurants
28+
}
29+
```
30+
31+
- The name of your selectors could be anything but it is a good practice to prefix them with `select` or `get` or end the name with `Selector` i.e. `selectCity`, `getRestaurant` or `ingredientsSelector`.
32+
- [Have a look at this Twitter poll on how to name selectors](https://twitter.com/_jayphelps/status/739905438116806656)
33+
34+
You may wonder why you should use selectors. The first reason is for encapsulation and reusability. For example, let's say one of our `mapStateToProps` function looks like:
35+
36+
```
37+
const mapStateToProps = (state) => {
38+
const data = state.some.deeply.nested.field;
39+
40+
return { data };
41+
}
42+
```
43+
44+
Accessing the data like above is absolutely fine but imagine that you require the same piece of information in several other components and when you make an API call. What will happen if you change the place where that piece of data live? You will have to find every instance where you have accessed that data and change it. So in the same way (action creators are used to encapsulate the details of creating actions)(https://blog.isquaredsoftware.com/2016/10/idiomatic-redux-why-use-action-creators/), we recommend to encapsulate the access of data in one place. 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.
45+
46+
## Selector performance
47+
48+
Once we have encapsulated our logic of accessing the data, the next thing to consider is performance. Imagine that we have a component that requires a very expensive operation(s) (filtering/sorting/transformation) before it gets its required information. For example:
49+
50+
```
51+
const mapStateToProps = state => {
52+
const { someData } = state;
53+
54+
const filteredData = expensiveFiltering(someData);
55+
const sortedData = expensiveSorting(filteredData);
56+
const transformedData = expensiveTransformation(sortedData);
57+
58+
return { data: transformedData };
59+
}
60+
```
61+
62+
Everytime our application state changes, the above `mapStateToProps` will run even if `someData` has not updated at all. What we really want to do in this situation is only execute our expensive operations when `someData` has changed. This is where the idea of _memoization_ comes in.
63+
64+
_Memoization_ is a form of caching where our `function` will be executed only when the `input` has changed. This means that if our `function` is called multiple times with the same `input` then it will return its cached result and not do any work.
65+
66+
One library that can help us with _memoization_ is [reselect](https://github.com/reduxjs/reselect). Let's first look at how `reselect` works:
67+
68+
```
69+
import { createSelector } from 'reselect'
70+
71+
const selectBurger = state => state.burger;
72+
const selectOrders = state => state.orders
73+
74+
// pass selectBurger and selectOrders as an array
75+
const selectBurgerOrdersTotalPrice1 = createSelector(
76+
[selectBurger, selectOrders],
77+
(burger, orders) => { // output selector
78+
return burger.price * orders.burgers;
79+
}
80+
)
81+
82+
// pass selectBurger and selectOrders as separate arguments
83+
const selectBurgerOrdersTotalPrice2 = createSelector(
84+
selectBurger,
85+
selectOrders,
86+
(burger, orders) => { // output selector
87+
return burger.price * orders.burgers;
88+
}
89+
)
90+
```
91+
92+
In our example, we want to derive the total price of all burger orders. We have two selectors `selectBurger` and `selectOrders` that give us the information about a single burger and the total number of orders respectively. Then we use `createSelector` from `reselect` to create a memoized selector function. As you can see there are several ways to create the selector - either pass the input selectors as an array or one after the other as separate arguments.
93+
94+
A good practice is to write our top level _input selectors_ as plain functions and use `createSelector` to create memoized selectors that look up nested values. Let's continue with our previous burger example. As you can see we are accessing the price of a burger and the total number of burger orders directly. So let's remedy that:
95+
96+
```
97+
// we keep selectBurger and selectOrders from the previous example.
98+
99+
const selectBurgerPrice = createSelector(
100+
[selectBurger],
101+
(burger) => burger.price
102+
);
103+
104+
const selectBurgerOrders = createSelector(
105+
[selectOrders],
106+
(orders) => orders.burgers
107+
);
108+
109+
const selectTotalBurgerOrdersPrice = createSelector(
110+
[selectBurgerPrice, selectBurgerOrders],
111+
(burgerPrice, burgerOrders) => {
112+
console.log('Calling output selector')
113+
return burgerPrice * burgerOrders;
114+
}
115+
);
116+
117+
const state = {
118+
burger: {
119+
price: 10
120+
},
121+
orders: {
122+
burgers: 5
123+
}
124+
};
125+
const result = selectTotalBurgerOrdersPrice(state);
126+
// Log: 'Calling output selector'
127+
console.log(result);
128+
// 50
129+
130+
const secondResult = selectTotalBurgerOrdersPrice(state);
131+
// No log output
132+
console.log(result);
133+
// 50
134+
```
135+
136+
Note that here the second time we called `selectTotalBurgerOrdersPrice` our output selector function did not execute. The reason for this is that `reselect` will execute all of the `input` functions that you have given as arguments and compare the results. If any of the results are different, it will rerun our output selector and if not will return the cached result. In our case since our burger price and the number of burger orders have not changed we have received the cached result of `50`. A good thing to keep in mind is that `reselect` will use `===` operator when comparing the results of the input selectors.
137+
138+
Another thing to consider is that by default `reselect` only memoizes the most recent set of parameters. That means that if you call the selector multiple times with different arguments it will keep calling the output selector every time. Let's look at an example:
139+
140+
```
141+
const tableOne = selectTableBill(state, 1); // first call with the state and the table number one. Not memoized.
142+
const tableOneAgain = selectTableBill(state, 1); // second call with the same state and the same table number one. The result is memoized.
143+
const tableTwo = selectTableBill(state, 2); // same state but different table number. Result is not memoized as the inputs are different.
144+
const chequeTableOneAgain = selectTableBill(state, 1); // same state but different table number because we have previously used table number 2.
145+
```
146+
147+
Also, you can pass multiple arguments in our selector and `reselect` will call each `selector` input function with the same arguments. That's why every `input` selector function should accept the same type of arguments. Otherwise the selector will break. For example:
148+
149+
```
150+
const selectOrders = (state) => state.orders;
151+
152+
// second argument is an object
153+
const selectDrinks = (state, table) => table.drinks;
154+
155+
// second argument is a number
156+
const selectTableGuests = (state, tableNumber) => guests;
157+
158+
const calculateBillForEachGuest = createSelector(
159+
[selectOrders, selectDrinks, calculateBillForEachGuest],
160+
(orders, drinks, guests) => ...do something...
161+
);
162+
163+
calculateBillForEachGuest(state, 1);
164+
```
165+
166+
In this example, our selector will break because we will try to call `1.drinks` in our `selectDrinks` selector.
167+
168+
## React and Reselect
169+
170+
Let's go back to our `mapStateToProps` example from earlier. We only wanted to execute our expensive operations only when our state has changed. So let's refactor that by using `reselect`:
171+
172+
```
173+
const selectSomeData = state => state.someData;
174+
175+
const selectFilteredSortedTransformedData = createSelector(
176+
[selectSomeData],
177+
(someData) => {
178+
const filteredData = expensiveFiltering(someData);
179+
const sortedData = expensiveSorting(filteredData);
180+
const transformedData = expensiveTransformation(sortedData);
181+
182+
return transformedData;
183+
}
184+
)
185+
186+
const mapState = (state) => {
187+
const transformedData = selectFilteredSortedTransformedData(state);
188+
189+
return { data : transformedData };
190+
}
191+
```
192+
193+
Our refactor of `mapStateToProps` will give us substantial performance improvements. There are two reasons for that:
194+
195+
- Our expensive operation will only run when `state.someData` has changed. So even if we `dispatch` an action that will update our `state.somethingElse`, we won't do any real work in our `mapStatetoProps`
196+
- When we connect our component to the `redux` store, our `connect` function determines if our component needs to rerender by doing a shallow equality comparing using `===` operator. Since our cached result is going to be the same our component is not rerendering.
197+
198+
Things to keep in mind are:
199+
200+
- Array functions like `concat()`, `map()`, and `filter()` as well as object spread operator will return a new reference which will result in our component to rerender if we use them in our `mapStateToProps` function.
201+
- Changing our whole state by creating a new object when an action is dispatch i.e. `{ ...previousState, orders: newOrders }`. Have a look at [Immer](https://github.com/mweststrate/immer) which can help us with making those changing and keeping our data immutable.
202+
203+
## Advanced Optimizations
204+
205+
There is a specific use case that you could come across which will have an impact on performance. For example, when we render the same component multiple times:
206+
207+
```
208+
const mapStateToProps = (state, ownProps) => {
209+
const bill = selectTableBill(state, ownProps.tableNumber);
210+
211+
return { bill };
212+
}
213+
214+
const BillComponent = props => <div>Bill: {props.bill}</div>;
215+
216+
export default connect(mapStateToProps)(BillComponent);
217+
218+
// render
219+
<BillComponent tableNumber={1} />
220+
<BillComponent tableNumber={2} />
221+
```
222+
223+
We are rendering our `<BillComponent />` twice and passing it a `tableNumber` prop which is the only difference between the two instances. In our `mapStateToProps` function, we are using our selector `selectTableBill` which is referring to the same selector instance in our code base. We have previously discussed that `reselect` has a size cache of one so in this situation when we render and re-render our `<BillComponent />` what will happen will be:
224+
225+
```
226+
selectTableBill(state, 1); // not memoized. Doing a memoization with state and table number 1.
227+
selectTableBill(state, 2); // it will not memoize as our table parameter is different.
228+
selectTableBill(state, 1); // it will not memoize as our table parameter is different.
229+
selectTableBill(state, 2); // it will not memoize as our table parameter is different.
230+
```
231+
232+
As you can see we are calling everytime our `selectTableBill` with different `inputs` and it will never be able to memoize properly.
233+
234+
To solve this `React-Redux connect` comes to the rescue. Both `mapStateToProps` and `mapDispatchToProps` accept _factory function_ syntax which means we can create a different instance of our `selectTableBill` and use it there. What that will look like will be:
235+
236+
```
237+
const makeSelectTableBill = () => createSelector(
238+
[selectTable],
239+
(items, itemId) => items[itemId]
240+
);
241+
242+
243+
const mapStateToProps = (state) => {
244+
const selectItemForThisComponent = makeSelectTableBill();
245+
246+
return function realMapStateToProps(state, ownProps) {
247+
const item = selectItemForThisComponent(state, ownProps.itemId);
248+
249+
return {item};
250+
}
251+
};
252+
253+
export default connect(mapStateToProps)(BillComponent);
254+
```
255+
256+
Our `mapStateToProps` is now a closure which creates a new instance of `selectTableBill` selector. This means that when our `<BillComponent />` are now rendered on the page each of them will have a unique instance of `selectTableBill` selector and be properly memoized.
257+
258+
##Links and References
259+
260+
- [Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance](https://blog.isquaredsoftware.com/2017/12/idiomatic-redux-using-reselect-selectors/)
261+
- [React/Redux Links: Redux Performance](https://github.com/markerikson/react-redux-links/blob/master/react-performance.md)

0 commit comments

Comments
 (0)