17

Improving Redux State Slicing Performance

 4 years ago
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.

vIR7b2Y.jpg

NG components with Bit: Instantly share, use and develop across projects

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 !!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK