Redux: Modeling State

http://briebug.github.io/presentations/conferences/2017/ng-mix/redux-modeling-state
Jesse Sanders
https://github.com/jessesanders
@BrieBugSoftware

What data should be kept in the Redux state?

  • The data is shared between different parts of the application.
  • The data is used by multiple components.
  • New data needs to be derived from the original data.
  • The state needs to be restored at a given point in time.
  • The data needs to be cached.

Types of data

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.

Organizing state in the store

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.

  • Duplicated data is difficult to update.
  • Nested data increases complexity of the reducer implementation…
  • …and may cause unnecessary rendering.

How

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' }
    }
  }
}
          
@ngrx/entity

Adapter

Provides methods for performing operations against a single collection of a specific type.

Instance Methods

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>();
          

Selectors

If the store is like a database…

…selectors are like queries.

@ngrx/store

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);
  }
}
          

References