Search code examples
javascriptreactjsreduxnormalizr

Normalize API data for use in Redux


I have the following data coming from an API:

const data = {
  id: 1,
  name: 'myboard',
  columns: [
    {
      id: 1,
      name: 'col1',
      cards: [
        { id: 1, name: 'card1' },
        { id: 2, name: 'card2' }
      ]
    },
    {
      id: 2,
      name: 'col2',
      cards: [
        { id: 3, name: 'card3' },
        { id: 4, name: 'card4' }
      ]
    },
  ]
}

As you can see, there are essentially 3 nested levels. The top level contains an id, name and a list of columns. Each column contains an id, name and a list of cards. Each card has an id and name.

I wish to normalize the data for use in Redux as presented here. I am using normalizr to do this as follows:

const card = new schema.Entity('cards');
const column = new schema.Entity('columns', {
  cards: [card]
});
const board = new schema.Entity('boards', {
  columns: [column]
});

normalize(data, board)

This results in the following:

{
  "entities": {
    "cards": {
      "1": {
        "id": 1,
        "name": "card1"
      },
      "2": {
        "id": 2,
        "name": "card2"
      },
      "3": {
        "id": 3,
        "name": "card3"
      },
      "4": {
        "id": 4,
        "name": "card4"
      }
    },
    "columns": {
      "1": {
        "id": 1,
        "name": "col1",
        "cards": [1, 2]
      },
      "2": {
        "id": 2,
        "name": "col2",
        "cards": [3, 4]
      }
    },
    "boards": {
      "1": {
        "id": 1,
        "name": "myboard",
        "columns": [1, 2]
      }
    }
  },
  "result": 1
}

What I can't seem to figure out is how to have each section (ie: cards, columns, boards) split into two sections, namely byId and allIds as per the Redux article referenced above.

Essentially this is to make ordering, sorting etc easier in a React application. Im using the latest version of normalizr (3.2.4).


Solution

  • Here is a CodeSandbox with an example of how you can set up the reducers to handle the normalized state.

    Essentially, you will end up with something like this for each of your entities:

    // lambda or function - whatever your preference is
    const cardsById = (state = {}, action) => {
      // if, case, handler function - whatever your preference is
      if (action.type === 'ADD_DATA') { // or whatever your initial data load type is
        return { ...state, ...action.payload.cards }
      }
      return state
    }
    
    const allCards = (state = [], action) => {
      if (action.type === 'ADD_DATA') { // or whatever your initial data load type is
        return [...state, ...Object.keys(action.payload.cards)]
      }
      return state
    }
    
    const cards = combineReducers({
      byId: cardsById,
      allIds: allCards
    })
    

    and then combine all of those together:

    export default combineReducers({
      cards,
      columns,
      boards
    })
    

    The action creators for this are as follows:

    const addData = ({ entities }) => ({
      type: 'ADD_DATA',
      payload: entities // note the rename - this is not required, just my preference
    })
    
    // I used a thunk, but theory is the the same for your async middleware of choice
    export const getData = () => dispatch => dispatch(addData(normalize(data, board)))
    

    Hope this helps. Remember that you will need to maintain both the byId and allIds for each entity as entities are added or removed.