Fat Global State Management .1(Selector)

kelly woo
4 min readFeb 7, 2021
Photo by Maria Teneva on Unsplash

Redux

Redux is a famous state management library, it offers centralized global store and helps maintain the states with consistency. But when the store gets bigger and bigger you get wonder, is this a good way to manage states shared among components as global state.

Redux is simple.
Reducer returns new state(or same one if no change requires) by the action dispatched and triggers listeners to notify its change.
In other words dispatch triggers this notification. Here’s the code.
https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L225

function dispatch(action: A) {    
...
if (isDispatching) {
throw new Error(
'Reducers may not dispatch actions.')
}
try {
isDispatching = true
// update state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}

So notification of change is not necessarily indicates actual changes on the state. The responsibility of check genuine event is left to View renderers.
Libraries like React and Angular(or so on…) put some effort to check the change on props, but mostly it is left to developers.

This is where selector comes in.

Selector

Selector is a delicate way of using memoization. If you’ve experienced React hooks, you’d get the main concept, same input yields same output.

We’d look into 2 libraries here, reselect and @ngrx/store.
They give same concept of createSelector.

CreateSelector gets pure functions(or selectors) as arguments and run the memoization.

Say the last function is combiner and the rest of functions before it propSelectors.

PropSelectors are to identify the ingredients to memoize and combiner is to compute and return real value that we need with these ingredients.

const propSelector1 = (state) => state.a;
const propSelector2 = (state) => state.b;
const combiner = (a, b) => a + b;const selector= createSelector(propSelector1, propSelector2, combiner);const value = selector({a: 2, b: 4, c: 3}); // 6// propSelector1 return 2 | propSelector1 return 4
// combiner gets those returns and computes them accordingly
// which results in 6;

If you pass new set of data{a: 2, b: 4, c:1} into selector,
combiner is not called since the arguments combiner gets(2, 4) are the same as before
and instead of recomputing, cached result from previous computing is returned. So we get ensured that same value gives same reference and to be free from shallowEqual changeDetection.

This is simple example, it is hard to recognize the power of memoization but when you use in complex computing, it will save lots of time and resources for re-renders.

These prop comparison is based on shallowEqual, and of course they provide a way to change this equal comparison. You can make custom selector with selectorFactory.

SelectorFactory

@ngrx/store named it createSelectorFactory and
reselect has createSelectorCreator.
To make customSelector import this selectorFactory and defaultMemoize.

Let’s say your component changes by the result of scale * price, not scale, price respectively, you can make selector for the multiplier.

interface State {
scale: number;
price: number;
}
const isEqual = (a, b) => a.scale * a.price === b.scale * b.price

@ngrx/store

import { createSelectorFactory, defaultMemoize } from "@ngrx/store";

const customSelector = createSelectorFactory((projectionFun) =>
defaultMemoize(projectionFun, isEqual)
);

reselect

import { createSelectorCreator, defaultMemoize } from "reselect";    

const reselectCustomSelector = createSelectorCreator(defaultMemoize, isEqual);

Use when and where

By using selectors you can produce the same output, and view libraries can easily check to render or not.
These are important tactic to reduce rendering time.

But you need to remember these selectors do not come free, they all keep cache(arguments, returned value), and still use computing for equal comparison. It is trade-off between computing and memory, choose wisely to use when and where.

  • I’d say featured selectors by the first props of store state aren’t usually good practice, something like below if it is combination of data.
const select = createSelector((state) => state.complex, state=> state); 
  • If selectors reference same state and same logic functions it is also good to share selectors. Especially between present components and containers since their data flow runs one way.
  • Selectors accept extra state.
    In this, reselect and ngrx chose different way.

reselect takes as many state as you pass but it is not passed to combiner.

const reselectSelector = createSelector(
(a, b, ..., d) => ...,
(a, b, ..., d) => ...,
(state1, state2) => ...
)

ngrx accept one prop, which is passed into propSelectors and combiner.

const ngrxSelector = createSelector(
(state, prop) => ...,
(state, prop) => ...,
(state1, state2, prop) => ...
)
// you can pass global state and component state at the same time
but in this case, do not share selector with other components.
// also props is the subject to identify change. if you put
{a : 1} and next time new instance of {a : 1}, it will recompute.

— — — — — — — — — — — — — — — — — — — — — — — — — — — —

Selector is to prevent unnecessary triggers by store, but what if, we don’t trigger view components at the first place, isn’t it better?

I think it is better. and that’s why I have local state stores for some parts.Let me continue this on next article.

Happy coding~

--

--