![](/style/images/good.png)
![](/style/images/bad.png)
Improving Redux State Slicing Performance
source link: https://www.tuicool.com/articles/VVVziuU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
R edux is the main state management pattern still used today. It puts our data in a global store and let components access it from anywhere in our app, they can modify and read from it at will.
In this post, we will look at improving performance on using Redux pattern in our apps.
Tip: Using Bit ( GitHub ) you can quickly share and reuse your Angular components across your apps. It will help your team build new apps faster, and save you time while collaborating and building with components. Try it.
State Slices
What are state slices?Much like taking a slice of orange :), that’s a very common usage of the word slice. But state slice is not far fetched from the orange slices.
Let’s say we have a state like this:
interface Book { name: String, author: String } const initailState = { user: String, allBooks: Array<Book> } const state = { user: 'nnamdi', allBooks: [ { name: 'Micro', author: 'Michael Crichton' }, { name: 'Jurassic Park', author: 'Michael Crichton' }, { name: 'The Lost World', author: 'Michael Crichton' } ] }
Our state has fields user
and allBooks
, now it won't be ideal to pass all these or the state as a whole to all components. If component A just makes use of the user
field of the state object, it needs to be passed the user
field only not the whole state object. Also, if a component deals with only allBooks
property it needs to be passed only the allBooks
array:
allBooks: [ { name: 'Micro', author: 'Michael Crichton' }, { name: 'Jurassic Park', author: 'Michael Crichton' }, { name: 'The Lost World', author: 'Michael Crichton' } ]
excluding the user
property.
The ngrx in Angular and react-redux in React provides a way whereby components can specify and take slices of the state they need.
In ngrx, components inject the Store service and call its select
method to take a slice of the state object.
@Component({ ... template: ` <div> <h3>List of Books</h3> <div *ngFor="let book of allBooks | async"> {{book}} </div> </div> ` }) export class App implements OnInit { constructor(private store: Store) {} ngOnInit() { this.allBooks = this.store.select('allBooks') } }
See, we tell the Store to give us the allBooks
property from the state object by calling the select
method with the property name 'allBooks'
.
In React we use the mapDispatchToProps
function:
function mapDispatchToProps(state) { return { allBooks: state.allBooks } }
The mapDispatchToProps
is just a name, the function can bear any name, it's just a convention to name function by what it does. This function takes a slice of the state object that maps it's to the props of the component that needs the slice.
Here, AllBooks
components need the allBooks
property array from the state object, so it defines a mapDispatchToProps
function that gets the slice from the state and maps it to its props:
class AllBooks extends Component { render() { return ( <> <div> <h3>List of Books</h3> {this.props.books.map(book => <div>book</div>)} </div> </> ) } } function mapDispatchToProps(state) { return { books: state.allBooks } } export default connect(mapDispatchToProps)(AllBooks)
See, we can access the allBooks array from props.books
property. We connect the AllBooks component to the store and mapStateToProps make it's possible for us to take a part of the state object and pass it to the component.
Projectors and Selectors
The function we used to take a particular state slice. In our ngrx app:
const selectAllBooks = (state) => addId(state.allBooks) const addId = (allBooks) => allBooks.map(book=> book + " - #"+Date.now()) @Component({ ... template: ` <div> <h3>List of Books</h3> <div *ngFor="let book of allBooks | async"> {{book}} </div> </div> ` }) export class App implements OnInit { constructor(private store: Store) {} ngOnInit() { this.allBooks = this.store.pipe(select(selectAllBooks)) } }
The selectAllBooks
function is our selector. addId
function is our projector, it transforms the data from the selector function.
We can’t only take a slice of the state, we can transform the state slice before returning it to the component. The transformation of the state slice before returning is done by the project
function or called projector.
Using our AllBooks
example
function addId(allBooks) { return allBooks.map(book=> book + ' - #'+Date.now()) } class AllBooks extends Component { render() { return ( <> <div> <h3>List of Books</h3> {this.props.books.map(book => <div>book</div>)} </div> </> ) } } function mapStateToProps(state) { return { books: addId(state.allBooks) } } export default connect(mapStateToProps)(AllBooks)
The mapStateToProps function is the selector function because it defines the property we want and the addId function is the projector because it transforms the state slice to add id tag at the end of each book name in the array.
All these work great, but there is a drawback. Have you considered if the selector function does an expensive calculation? Let’s say it takes 1min to do a calculation, that’s overly expensive and will result in poor performance.
Now, whenever the state object is updated, the selectors will be run. Note this, whenever a state is updated, re-render of the component tree is triggered, the selectors are run, then if the component is memoized it will check against the prev value. The point here is that even if our component is memoized the selectors will still be run because the memoization algo. will need the current state slice to check against the prev value.
We have a state:
const state = { user: 'nnamdi', allBooks: [ { name: 'Micro', author: 'Michael Crichton' }, { name: 'Jurassic Park', author: 'Michael Crichton' }, { name: 'The Lost World', author: 'Michael Crichton' } ] }
Then we have components AllBooks and User, they take a slice of allBooks and user respectively. If User component updates the state object to:
const state = { user: 'nnamdi chidume', allBooks: [ { name: 'Micro', author: 'Michael Crichton' }, { name: 'Jurassic Park', author: 'Michael Crichton' }, { name: 'The Lost World', author: 'Michael Crichton' } ] }
React will try to update both components then React-Redux internal memoization will run to know which component to update. Selectors of both components will be run using this new state slice React-Redux will try to memoize. Now, User component state slice will be:
{
user: 'nnamdi chidume'
}
AllBooks state will be:
{
allBooks: [
{
name: 'Micro',
author: 'Michael Crichton'
},
{
name: 'Jurassic Park',
author: 'Michael Crichton'
},
{
name: 'The Lost World',
author: 'Michael Crichton'
}
]
}
See AllBooks
state slice didn't change but User state slice changed so the selector for the User component should be run, selector for AllBooks should not be run because its state slice remained the same/didn't change.
You see now that Redux state slicing will become a huge performance in our application. How do we rectify this? We have to memoize the selectors so that they will be run whenever the state objects changes.
Libraries have been built to memoize/optimize our state selectors, the most popular and most used is the reselect
library.
ngrx has it’s own custom selector functionality built into it.
We will look at them below.
Using reselect
library
Simple “selector”� library for Redux (and others) inspired by getters in NuclearJS , subscriptions in re-frame and this proposal from speedskater .
reselect optimizes state selection by memoizing the state and the derived state. First, to use the reselect library we have to install it:
npm i reselect
The reselect library exports the createSelector function it is the most important function in the library, this function takes one or more selector function(s) and a projector function.
createSelector(...inputSelectors | [inputSelectors], resultFunc)
It returns a function which will take a state object. reselect caches the result of the inputSelectors functions and checks if their values have changed since the last run using the reference operator ===
.
If we have selectors like this:
const allBooksState =(state) => state.allBooks const userState =(state) => state.user
See the functions takes slices of allBooks and user properties from the state object. Now, lets define a projector function:
(allBooks, user) => allBooks.map(book => {...book, ...user})
The projector just merges the state slices allBooks and user, we pass the selectors first to createSelector and last, the projector:
const userAllBooksSelector = createSelector(allBooksState,userState, (allBooks, user) => allBooks.map(book => {...book, ...user}) )
The createSelector function returns a higher-order function which will be stored in the userAllBooksSelector variable. When we call the userAllBooksSelector with the state:
const state = { user: 'nnamdi', allBooks: [ { name: 'Micro', author: 'Michael Crichton' }, { name: 'Jurassic PArk', author: 'Michael Crichton' }, { name: 'The Lost World', author: 'Michael Crichton' } ] } userAllBooksSelector(state)
Output:
[
{
name: 'Micro',
author: 'Michael Crichton',
user: 'nnamdi'
},
{
name: 'Jurassic Park',
author: 'Michael Crichton',
user: 'nnamdi'
},
{
name: 'The Lost World',
author: 'Michael Crichton',
user: 'nnamdi'
}
]
The selectors will be run because the prev cache will be null. Now the cache will hold the state object. If we run the function without changing the state:
userAllBooksSelector(state)
The selectors and projector won’t be run because reselect has seen the state object before. If we change the state object:
const newAllBooks = state.allBooks.concat([ { name: 'Congo', author: 'Michael Crichton' } ]) state = {...state, ...newAllBooks}
The allBooks selector will be run because the allBooks array was changed. Reselect works on and supports immutability on data structures.
If you mutate your data structure reselect won’t pick up the change because it checks for changes using ===
, this operator works differently on data structures. We have a primitive data structure and complex data structure.
Primitives are:
- Boolean
- Number
- String
Complexes are:
- Arrays
- Objects
===
refers to primitives by their values but refer to complexes by their references(memory addresses). So two complex data structures are deemed equal if their references are the same, not by the data they hold. So we should make sure we don't mutate our state because the selectors and projectors will not be re-run which will lead to the display of wrong data.
We can use the reselect
library to memoize our React components mapStateToProps
function. Let's take our AllBooks
component:
function addId(allBooks) { // No mutation of the `allBooks` array, `map` returns a new Array return allBooks.map(book=> book + ' - #'+ Date.now()) } class AllBooks extends Component { render() { return ( <> <div> <h3>List of Books</h3> {this.props.books.map(book => <div>book</div>)} </div> </> ) } } function mapStateToProps(state) { return { books: addId(state.allBooks) } } export default connect(mapStateToProps)(AllBooks)
If the allBooks
array has thousands of inputs, we can't afford our map
function running just for React-Redux
to check for any changes. We will compose the addId
function with the createSelector
function:
const addId = createSelector(state=> state.allBooks, allBooks=> allBooks.map(book=> book + ' - #' + Date.now()));class AllBooks extends Component { render() { return ( <> <div> <h3>List of Books</h3> {this.props.books.map(book => <div>book</div>)} </div> </> ) } } function mapStateToProps(state) { return { books: addId(state) } } export default connect(mapStateToProps)(AllBooks)
Now, the addId
selector will only be run when the allBooks
array in the state object changes (a referential change).
Using createSelector
in ngrx/Angular
The createSelector
in ngrx works the same as the reselect#createSelector
.
Like we said earlier, selectors are methods used for obtaining slices of state. The createSelector
functions keep track of the latest arguments in which our selector was invoked with. These cached arguments are returned when a new arguments match the prev arguments without invoking the selector function. This provides huge performance benefits when the selector functions perform heavy calculations.
Let’s look at our App component:
const selectAllBooks = (state) => addId(state.allBooks) const addId = (allBooks) => allBooks.map(book=> book + " - #"+Date.now()) @Component({ ... template: ` <div> <h3>List of Books</h3> <div *ngFor="let book of allBooks | async"> {{book}} </div> </div> ` }) export class App implements OnInit { constructor(private store: Store) {} ngOnInit() { this.allBooks = this.store.pipe(select(selectAllBooks)) } }
Optimizing with createSelector
function, first import it from the @ngrx/store
library:
import { createSelector } from '@ngrx/store';
Then we compose the selector like this:
const selectAllBooks = createSelector(state => state.allBooks, allBooks => allBooks.map(book=> book + " - #" + Date.now()));@Component({ ... template: ` <div> <h3>List of Books</h3> <div *ngFor="let book of allBooks | async"> {{book}} </div> </div> ` }) export class App implements OnInit { constructor(private store: Store) {} ngOnInit() { this.allBooks = this.store.pipe(select(selectAllBooks)) } }
Conclusion
Optimizing our selectors is a very promising way to go. Remember, you don’t have to optimize if you don’t have performance problems. If you do, you’re doing premature optimization. Instead, start by writing readable code and follow best practices, then finally optimize.
Make sure you try to write fast JS code in your selectors and projectors before trying to optimize them.
If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.
Thanks !!!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK