What data should be kept in the Redux state?
The types of data that applications typically deal with can be broadly divided into three categories.
Domain data
Data that the application needs to show, use, or modify.
Example: the list of todo items (fetched from the server).
Application state
Data that is specific to the application's behavior.
Example: todo items 5 and 8 are selected.
Example: the request to fetch the todo list is in progress.
UI state
Data that represents how the UI is currently displayed.
Example: the sidebar is open.
Avoid defining your state shape in terms of the UI component tree.
Define the state shape in terms of the domain data and application state.
{
domainData1 : {},
domainData2 : {},
appState1 : {},
appState2 : {},
ui : {
uiState1 : {},
uiState2 : {},
}
}
Normalize the data.
Why?
const blogPosts = [
{
id: 'post1',
author: { name: 'User One' },
body: '...',
comments: [
{ author: { name: 'User Two' }, comment: '...' },
{ author: { name: 'User Three' }, comment: '...' },
{ author: { name: 'User One' }, comment: '...' }
]
}
];
Data structure is complex.
Data is repeated.
Each type of data gets its own "table"
{
posts: { ... },
comments: { ... },
users: { ... }
}
Each "table" is an object. For each item, the item ID is the key and the value is item.
{
posts: {
entities: {
post1: { ... },
post2: { ... }
}
},
...
}
Item order is stored in an array containing the item IDs.
{
posts: {
entities: {
post1: { ... },
post2: { ... }
},
ids: ['post1', 'post2']
},
...
}
Related items are referenced by ID.
{
posts: {
entities: {
post1: { author: 'user1' }
}
},
users: {
entities: {
user1: { name: 'User One' }
}
}
}
Provides methods for performing operations against a single collection of a specific type.
addOne, addMany, addAll, removeOne, removeMany, removeAll, updateOne, updateMany
Interface: EntityState
interface EntityState<V> {
ids: string[];
entities: { [id: string]: V };
}
export interface User {
id: string;
name: string;
}
export interface State extends EntityState<User> {
// additional entity state properties
selectedUserId: number | null;
}
Interface: EntityAdapter
export const adapter: EntityAdapter<User> =
createEntityAdapter<User>();
If the store is like a database…
…selectors are like queries.
createSelector
createFeatureSelector
Example: Select a root property
import { createSelector, createFeatureSelector } from '@ngrx/store';
export interface FeatureState {
counter: number;
}
export interface AppState {
feature: FeatureState;
}
export const selectFeature =
(state: AppState) => state.feature;
Example: Compose multiple selectors
export const selectFeatureCount =
createSelector(
selectFeature,
(state: FeatureState) => state.counter
);
Example: Compute data
const selectItems = state => state.items;
const selectTotal = createSelector(selectItems,
(items) => items.reduce((acc, item) => acc + item.value, 0));
const state = {
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 }
]
};
selectTotal(state); // 2.15
Using a selector with the Store
import ( selectTotal } from '../reducers/feature';
@Component({
selector: 'my-app',
template: `
<div>Total: {{ total | async }}</div>
`
})
class MyAppComponent {
total: Observable<number>;
constructor(private store: Store<AppState>) {
this.total = store.select(selectTotal);
}
}