Enter Your Email Address to Watch This Lesson

Your link to unlock this lesson will be sent to this email address.

Unlock this lesson and all 827 of the free egghead.io lessons, plus get JavaScript content delivered directly to your inbox!



Existing egghead members will not see this. Sign in.

Just one more step!

Check your inbox for an email from us and click link to unlock your lesson.



Redux: Refactoring the Reducers

6:23 JavaScript lesson by

We will learn how to remove the duplication in our reducer files and how to keep the knowledge about the state shape colocated with the newly extracted reducers.

Get the Code Now
click to level up

egghead.io comment guidelines

Avatar
egghead.io

We will learn how to remove the duplication in our reducer files and how to keep the knowledge about the state shape colocated with the newly extracted reducers.

Avatar
Ben

looks like a given component (or container, or equivalent) could ultimately have its own reducer, doesn't it ?

Avatar
Dan Abramov

If your components map 1:1 to reducers, maybe you don’t need Redux, and could use Redux state model instead :-). In this tutorial, the structure of components doesn’t neatly map to the structure of reducers. This has benefits such as ensuring entities are never duplicated in the state, but it also means components do not “have” their own reducers.

In reply to Ben
Avatar
Puneeth

Here you mention we write the selector function in the same file as the reducer. But if we were to make use of selector library like reselect, should the selector functions be written in a separate file? What would be a good way to go about it?
Thanks

Earlier, we removed the visibilityFilter reducer, and so the root reducer in the app now combines only a single todos reducer. Since index.js acts effectively as a proxy to the todos reducer, I will just remove index.js completely, and I will rename todos.js to index.js, thereby making the todos my new root reducer.

The root reducer file now contains byId, allIds, activeIds, and completedIds, and I'm going to extract some of them into separate files. I'm starting with byId reducer, and I'm creating the file called byId.js, where I paste this reducer and export it as a default export.

byId.js

const byId = (state = {}, action) => {
  switch (action.type) {
    case 'RECEIVE_TODOS':
    const nextState = { ...state };
    action.response.forEach(todo => {
      nextState[todo.id] = todo;
    });
    return nextState;
  default:
    return state;
  }
}

export default byId;

I'm also adding a named export for a selector called getTodo, that takes the state and id, where the state corresponds to the state of byId reducer.

byId.js

export const getTodo = (state, id) => state[id];

Now I'm going back to my index.js file, and I can import the reducer as a default import, and I can import any associated selectors in a single object with a namespace import.

index.js

import byId, * as fromById from './byId';

If we take a look at the reducers managing the IDs, we will notice that their code is almost exactly the same except for the filter value which they compare action filter to. I will create a new function called createList that takes filter as an argument.

It returns another function, a reducer that handles the IDs for the specified filter, so its state shape is an array, and I can copy-paste the implementation from allIds. I just need to change the all literal here to the filter argument to the createList function, so that we can create it for any filter.

index.js

const createList = (filter) => {
  return (state = [], action) => {
    if (action.filter !== filter) {
      return state;
    }
    switch (action.type) {
      case 'RECEIVE_TODOS':
        return action.response.map(todo => todo.id);
      default:
        return state;
    }
  };
};

Now I can remove the allIds, activeIds, and completedIds reducer code completely. Instead, I will generate the reducers using the new createList function I wrote, and passing the filter as an argument to it.

index.js

const idsByFilter = combineReducers({
  all: createList('all'),
  active: createList('active'),
  completed: createList('completed'),
})

Next, I will extract the createList function into a separate file, just like I extracted the byId reducer. I added a new file called createList.js. I pasted my createList function there, and I will export it as a default export. Now that it's in a separate file, I'm adding a public API for accessing the state in form of a selector. That is called getIds, and for now, it just returns the state of the list, but this may change in the future.

createList.js

const createList = (filter) => {
  return (state = [], action) => {
    if (action.filter !== filter) {
      return state;
    }
    switch (action.type) {
      case 'RECEIVE_TODOS':
        return action.response.map(todo => todo.id);
      default:
        return state;
    }
  };
};

export default createList;

export const getIds = (state) => state;

Now I will go back to my index.js file, and I will import createList just like I usually import reducers, and I will import any named selectors from this file, as well.

index.js

import creatList, * as fromList from './createList';

I'm renaming the Ids by filter reducer to listByFilter, because now that the list implementation is in a separate file, I'll consider its state structure to be opaque, and to get the list of IDs, I will use the get IDs selector that it exports.

index.js

const idsByFilter = combineReducers({
  all: createList('all'),
  active: createList('active'),
  completed: createList('completed'),
})

const todos = combineReducers({
  byID,
  listByFilter,
});

export const getVisibleTodos = (state, filter) => {
  const ids = fromList.getIds(state.listByFilter[filter]);
  return ids.map(id => fromById.getTodo(state.byId[id]);
};

Since I also moved the byId reducer into a separate file, I also don't want to make an assumption that it's just a lookup table, and I will use fromById.getTodo selector that it exports and pass its state and the corresponding id.

This lets me change the state shape of any reducer in the future without rippling changes across the code base. Let's recap how we refactored the reducers.

First of all, the todos reducer is now the root reducer of the application, and its file has been renamed to index.js.

index.js

const todos = combineReducers({
  byID,
  listByFilter,
});

We extracted the byId reducer into a separate file. The byId reducer is now declared here, and it is exported from this module as a default export.

byid.js

const byId = (state = {}, action) => {
  switch (action.type) {
    case 'RECEIVE_TODOS':
    const nextState = { ...state };
    action.response.forEach(todo => {
      nextState[todo.id] = todo;
    });
    return nextState;
  default:
    return state;
  }
}

export default byId;

To encapsulate the knowledge about the state shape in this file, we export a new selector that just gets the todo by its ID from the lookup table.

byid.js

export const getTodo = (state, id) => state[id];

We also created a new function called createList that we use to generate the reducers, managing the lists of fetched todos for any given filter.

byid.js

const idsByFilter = combineReducers({
  all: createList('all'),
  active: createList('active'),
  completed: createList('completed'),
})

Inside createlist.js, we have a createList function that takes filter as an argument, and it returns a reducer that manages the fetched IDs for this filter.

The generated reducers will handle the RECEIVE_TODOS action, but they will keep any action that has the filter different from the one they were created with.

createList.js

const createList = (filter) => {
  return (state = [], action) => {
    if (action.filter !== filter) {
      return state;
    }
    switch (action.type) {
      case 'RECEIVE_TODOS':
        return action.response.map(todo => todo.id);
      default:
        return state;
    }
  };
};

CreateList is the default export from this file, but we also export a selector that gets the ids from the current state. Right now, the ids are the current state, but we are free to change this in the future.

In index.js, we use the namespace import syntax to grab all selectors from the corresponding file into an object.

index.js

import { combineReducers } from 'redux';
import byId, * as fromById from './byId';
import creatList, * as fromList from './createList';

The listByFilter reducer combines the reducers generated by createList, and it uses the filters as the keys.

index.js

const listByFilter = combineReducers({
  all: createList('all'),
  active: createList('active'),
  completed: createList('completed'),
})

Because listByFilter is defined in this file, the getVisibleTodo selector can make assumptions about its state shape and access it directly. However, the implementation of createList is in a separate file, so this is why it uses the fromList.getIds selector to get the IDs from it.

index.js

export const getVisibleTodos = (state, filter) => {
  const ids = fromList.getIds(state.listByFilter[filter]);
  return ids.map(id => fromById.getTodo(state.byId[id]);
};

Now that we have the IDs, we want to map them to the todos inside the state.byId. But now that it's in a separate file, rather than reach into it directly, we use the selector that it exports to access the individual todo.

Redux does not enforce that you encapsulate the knowledge about the state shape in particular reducer files. However, it's a nice pattern, because it lets you change the state that is stored by reducers without having to change your components or your tests if you use selectors together with reducers in your tests.



HEY, QUICK QUESTION!
Joel's Head
Why are we asking?