A Useful Redux Pattern

A side project I had been working on involved using React and Redux to make a simple application. The hardest part of the project was wrestling the documentation, which is very verbose, yet lacking in useful examples. Redux, a popular state management framework that implements the flux design pattern, has some of the most laughable documentation of all.

At the high level, what Redux is supposed to do is store all the data in a single location so that the components can all be generated from a single location. Considering that applications can have very complex data stored within them, one would reasonably assume that the documentation and examples would explain how to structure one’s code. This isn’t accurate; examples commonly show how to track state for a single scalar value, and the pages that talk about advanced state do not include examples. They briefly talk about normalization of data, and while they show sample data, they do not show how this fits into a larger project.

On my own, I came up with a pattern to solve this problem. Maybe someone else already did it, or maybe there is documentation recommending this pattern elsewhere. Let’s consider an example slice, called fruit, that contains a number of attributes per fruit in a table that one would expect to see in a database. Here’s what I would write to store it in redux:

import {
  createSlice,
  PayloadAction
} from "@reduxjs/toolkit";

export interface FruitModel {
  fruitKey: number;
  commonName: string;
  latinName: string;
  isCitrus: boolean;
  shelfLife: number;
}

interface FruitState {
  fruits: FuitModel[];
  fruitKey: number;
}

const initialState: FruitState = {
  fruits: [],
  fruitKey: 0,
};

export const fruitSlice = createSlice({
  name: "fruit",
  initialState,

  reducers: {
    addDefaultFruit: ({ fruits, fruitKey, ...state }) => ({
      ...state,
      fruits: [
        ...fruits,
        {
          fruitKey,
          commonName: "",
          latinName: "",
          isCitrus: false,
          shelfLife: 0,
        },
      ],
      fruitKey: fruitKey + 1,
    }),

    /*
     * Add more reducers here!
     */
  },
});

export const { addDefaultFruit } = fruitSlice.actions;
export default fruitSlice.reducer;

The fruit table’s schema is defined in FruitModel. There is a global fruitKey value that indicates the key of the next-created fruit, and each fruit object contains its own fruitKey which serves as its primary key. The key also makes life a lot easier when iterating over the fruits to make a list, as it can fit easily into the iteration’s key value. That way, if one ever needs to deal with re-orderable objects, there is an easy way to identify the objects as unique.

This also makes it easy to have objects that contain other objects. If I were to add a Basket slice to the project, where each basket has many fruits, I can simply add a basketKey to my fruit schema, and just make that a required payload for any action that adds a new fruit.

addDefaultFruitToBasket: (
  { fruits, fruitKey, ...state },
  { payload : { basketKey } }: PayloadAction<BasketModel>
) => ({
  ...state,
  fruits: [
    ...fruits,
    {
      fruitKey,
      basketKey,
      commonName: "",
      latinName: "",
      isCitrus: false,
      shelfLife: 0,
    },
  ],
  fruitKey: fruitKey + 1,
}),

This means it is possible to perform queries against the store, where one can select all children from a particular basket. It is also possible to make it so that all children can follow actions of its parents. This is useful to collect garbage if a parent object is removed; Fruits can respond to a theoretical removeBasket action and remove themselves if their basketKey matches the basket being removed.

One current drawback is if your schema includes multiple nested relationships, either one needs to store the keys of all parents at all levels (i.e. if A has many B has many C, C would need a copy of both B’s key and A’s key) or middleware ir required to pump events down to child objects.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.