This series explores how we can keep our code declarative as we adapt our features to progressively higher levels of complexity.
Level 4: Reusable State Patterns
Look what happens when we take out the object that manages our state:
export class ColorsComponent {
adapter = createAdapter<string[]>({ // Added `createAdapter`
changeColor: (colors, [newColor, index]: [string, number]) =>
colors.map((color, i) => i === index ? newColor : color),
selectors: {
colors: state => state.map(color => ({
value: color,
name: color.charAt(0).toUpperCase() + color.slice(1),
})),
},
});
store = createStore('colors', ['aqua', 'aqua', 'aqua'], this.adapter);
}
Do you see how this simple change enables us to handle a lot more complexity? We could easily create several stores for several independent lists of colors:
favoriteStore = createStore('colors.favorite', ['aqua', 'aqua', 'aqua'], this.adapter);
dislikedStore = createStore('colors.disliked', ['orange', 'orange', 'orange'], this.adapter);
neutralStore = createStore('colors.neutral', ['purple', 'purple', 'purple'], this.adapter);
That's pretty hideous, but that's on the designer, not us.
I called the state manager object adapter
because NgRx calls its entity state manager entityAdapter
.
NgRx/Entity is thought of as an advanced pattern of NgRx, but in our case, creating our colors adapter was the most minimal and simple way to describe the state of colors
. We just defined it inline first. Minimal code is flexible.
Every web app could eventually grow in complexity to the point where it has multiple instances of the same type of state on the same page. I have encountered it a few times. It sucked every time. We are just dealing with colors, but once on a large project a designer had me add a 2nd filtered, paginated data grid on the same page, and suddenly our state management solution—which had coupled specific user events, specific instances of state, and business logic for state changes and derived state, all together—became a big refactoring project. Action payloads had to change, and the state object had to become more deeply nested. It took time and made the code uglier and harder to understand.
Using something other than state adapters for state logic is a syntactic dead end. It might not come up as often as other problems, but it does come up, and it is usually unpredictable. So, no matter what state management library we end up using, we should keep our state logic bundled together, not too tightly coupled to events or side-effect logic. I personally think the adapter syntax is best.
createAdapter
is something I've implemented that provides type inference. It's pretty simple to implement, but all you really need is an object that holds state change functions and selectors. You don't need any library for that.
Next we'll talk about asynchronous sources of state.
Top comments (0)