Generalising models via helpers

You may identify repeated patterns within your store implementation. It is possible to generalise these via helpers.

For example, say you had the following:

const store = createStore({
  products: {
    data: {},
    ids: computed(
      [state => state.data],
      (resolvedState) => {
        const [data] = resolvedState;
        return Object.keys(state.data)
      }
    ),
    fetched: action((state, products) => {
      products.forEach(product => {
        state.data[product.id] = product;
      });
    }),
    fetch: thunk(async (actions) => {
      const data = await fetchProducts();
      actions.fetched(data);
    })
  },
  users: {
    data: {},
    ids: computed(
      [state => state.data],
      (resolvedState) => {
        const [data] = resolvedState;
        return Object.keys(state.data)
      }
    ),
    fetched: action((state, users) => {
      users.forEach(user => {
        state.data[user.id] = user;
      });
    }),
    fetch: thunk(async (dispatch) => {
      const data = await fetchUsers();
      actions.fetched(data);
    })
  }
})

You will note a distinct pattern between the products and users. You could create a generic helper like so:

const dataModel = (endpoint) => ({
  data: {},
  ids: computed(
    [state => state.data],
    (resolvedState) => {
      const [data] = resolvedState;
      return Object.keys(state.data)
    }
  ),
  fetched: action((state, items) => {
    items.forEach(item => {
      state.data[item.id] = item;
    });
  }),
  fetch: thunk(async (actions, payload) => {
    const data = await endpoint();
    actions.fetched(data);
  })
})

You can then refactor the previous example to utilise this helper like so:

const store = createStore({
  products: {
    ...dataModel(fetchProducts)
    // attach other state/actions/etc as you like
  },
  users: {
    ...dataModel(fetchUsers)
  }
})

This produces an implementation that is like for like in terms of functionality but far less verbose.

TypeScript version

We can utilise TypeScript to create model helpers too. Here is the same example adapted for TypeScript.

export interface ObjectWithId {
  id: string;
}

export interface DataModel<DataItem extends ObjectWithId> {
  data: { [key: string]: DataItem };
  ids: Select<DataModel<DataItem>, string[]>;
  fetch: Thunk<DataModel<DataItem>>;
  fetched: Action<DataModel<DataItem>, DataItem[]>;
}

export const dataModel = <Items extends ObjectWithId>(
  endpoint: () => Promise<Items[]>
): DataModel<Items> => ({
  data: {},
  ids: select(state => Object.keys(state.data)),
  fetched: (state, items) => {
    state.data = items;
  },
  fetch: thunk(async (actions, payload) => {
    const data = await endpoint();
    actions.fetched(data);
  })
});