React-Redux: Object Based Reducers

GitHub Repos

💾 Ncoughlin: React-Streams-Client

💾 Ncoughlin: React-Streams-API

Intro

This post continues our series creating a Twitch Clone called “Glitch”. We are in the process of creating CRUD operations for video streams using an external API server and Redux. We have created actions for our CRUD operations, and now must create reducers for those actions.

We have previously discussed array based reducers here: Ncoughlin: Intro to Redux #reducers

Now we are going to use reducers to return objects, and use key value pairs to target specific streams inside of those objects. Naturally we will use the stream ID as the key:value pair for these objects.

Reducer Update Strategies

Just as a refresher, here is our list of strategies to manipulate arrays and objects in reducers. Remember that the goal is to create a new copy of the original data, and manipulate it in some way. We never send back the original array/object to the store, it must always be replaced with a new one.

Array Manipulation

Bad Good
Removing an element from an array state.pop() state.filter(element => element !== 'hi')
Adding an element to an array state.push('hi') [...state, 'hi']
Replacing an element in an array state[0] = 'hi' state.map(el => el === 'hi' ? 'bye' : el)

Object Manipulation

Bad Good
Updating a property in an object state.name = 'Sam' {...state, name: 'Sam'}
Adding a property to an object state.age = 30 {...state, age: 30 }
Removing a property from an object delete state.name { age: omit, ...newState } = state

Scaffolding the Streams Reducer

Reducing Single Records

So understanding the object manipulation methods above and that we are going to be using the stream ID as the key:value pair, we can go ahead and scaffold out the streams reducer and create our first case for editing a stream like so.

reducers/streamReducer.js
import {
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  EDIT_STREAM,
  DELETE_STREAM,
} from "../actions/types"

const streamReducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_STREAMS:

    case FETCH_STREAM:

    case CREATE_STREAM:

    case EDIT_STREAM:
      // copy state object
      const newState = { ...state }      // locate and overwrite specific item
      newState[action.payload.id] = action.payload      // return new state
      return newState
    case DELETE_STREAM:

    default:
      return state
  }
}

export default streamReducer

So we began by creating the new object as we always do, as an exact copy of the original object. Then we targeted a specific item in that object using the id that came in on the action payload, and we changed it to the current payload. And returned our new state.

Key Interpolation

There is however some ES2015 syntax that we can use to shorten these three operations to one line.

reducers/streamReducer.js
import {
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  EDIT_STREAM,
  DELETE_STREAM,
} from "../actions/types"

const streamReducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_STREAMS:

    case FETCH_STREAM:

    case CREATE_STREAM:

    case EDIT_STREAM:
      return { ...state, [action.payload.id]: action.payload }
    case DELETE_STREAM:

    default:
      return state
  }
}

export default streamReducer

So these two examples are perfectly equivalent. One of the confusing parts here is the [action.payload.id], which looks like it is creating an array, but it is not. It is something known as key interpolation. We don’t know what key we want to add to the object ahead of time, so we can use this method to have a variable for the key. And then we assign a value to that identified item using the standard key:value pair syntax.

Knowing this we can fill out a couple more of our reducers that are handling single records.

reducers/streamReducer.js
import {
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  EDIT_STREAM,
  DELETE_STREAM,
} from "../actions/types"

const streamReducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_STREAMS:

    case FETCH_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    case CREATE_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    case EDIT_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    case DELETE_STREAM:

    default:
      return state
  }
}

export default streamReducer

Remember what is happening here. The reducer is going to update state records in the Store, which is then passing the state down to components as props. So we are just telling the store what scope of it’s state is going to be modified in each case here. In each case above we are requesting information on a single record. The Action is where we differentiate between fetching, creating and editing, not here in the Reducer. The reducer is defining scope.

Deleting Single Record

The syntax for deleting one key in the object can be a little tricky. There is a good Stack Overflow thread on this here:

StackOverflow: Clone a JavaScript Object except for one key

Which would give us the following.

reducers/streamReducer.js
import {
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  EDIT_STREAM,
  DELETE_STREAM,
} from "../actions/types"

const streamReducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_STREAMS:

    case FETCH_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    case CREATE_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    case EDIT_STREAM:
      return { ...state, [action.payload.id]: action.payload }

    // delete payload consists ONLY of ID
    case DELETE_STREAM:
      const { [payload]: omit, ...newState } = state      return newState
    default:
      return state
  }
}

export default streamReducer

and the one part here that might be confusing is that we did not use payload.id because in our action we ONLY sent the ID in the payload, so we do not have to specify a property of the payload. It only includes the ID.

Fetch Streams

The last reducer that we need to take care of is to fetch the list of streams. There are some things to discuss here before we get into this. The first thing to understand is that our API server is holding the streams in an array, and our store is holding all of them in an object. An object that contains lots of other objects. So we are going to have to convert from an array of objects to an object… of objects. The second thing that we need to consider is whether or not we want to completely overwrite the whole list of streams with the response from the API server, or whether we want to merge the streams from the API with our current list in the store.

It really depends on our use case. For example, if we were doing something like creating an infinite scroll list, we would want to merge the results from the server with our current list and append them to the end. However if we were treating the server like the canonical source of truth for the list of servers we would to replace our current store.

In the case of this application, in the real world we would want to replace the entire list of streams, so that if another user shut down their stream, we would not still be able to see it, as it would have been removed from the canonical source list. However, because this is just a work sample, and because replacing the list is the simpler operation of the two options, we are going to go through the process of merging the list, so that we know how to do that for future reference.

In short we are going to be doing two things here:

  • Convert Array of data to Object
  • Merge new Streams Object on our existing Streams Object

To do this we are going to use a little lodash function called mapKeys(). There are ways to do this with vanilla JavaScript, but lodash makes this easy. The mapKeys function takes two arguments, the first is the array, and the second is the property that we want to use as the key for each child object. So if we put these two things together we get the following.

reducers/streamReducer.js
import {
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  EDIT_STREAM,
  DELETE_STREAM,
} from "../actions/types";

import _ from "lodash";

const streamReducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_STREAMS:
      return  {...state, ..._.mapKeys(action.payload, 'id')}
    case FETCH_STREAM:
      return { ...state, [action.payload.id]: action.payload };

    case CREATE_STREAM:
      return { ...state, [action.payload.id]: action.payload };

    case EDIT_STREAM:
      return { ...state, [action.payload.id]: action.payload };

    // delete payload consists ONLY of ID
    case DELETE_STREAM:
      const { [payload]: omit, ...newState } = state;
      return newState;

    default:
      return state;
  }
};

export default streamReducer;

and then we add our now completed streams reducer to our reducers index.

reducers/index.js
import { combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";

import authReducer from "./authReducer";
import streamReducer from "./streamReducer";
export default combineReducers({
  auth: authReducer,
  streams: streamReducer,
  form: formReducer, // key "form" is mandatory
});