0
votes

I'm writing reducers for my application using ngrx/store.

Here is the layout of my application state:

{
    project: {
        name: string
        tasks: Array<Task>
    }
}

with:

interface Task {
    name: string
}

I'm trying to write clean reducers in separate files.

Here is the solution i'm currently using:

project.reducer.ts

import {tasksReducer} from './tasks.reducer';

const projectReducer = (state:Project = null, action: Action): Project => {
    switch (action.type) {
        case 'CREATE_PROJECT':
            return {
                name :'New project',
                tasks: []
            };
    };

    state.tasks = tasksReducer( state.tasks, action );

    return state;
}

tasks.reducer.ts

export const tasksReducer = (state:Array<Task> = [], action: Action): Array<Task> => {
    switch (action.type) {
        case 'ADD_TASK':
            return [...state, { name: 'New task' ];
        default:
            return state;
    };
}

Store is provided using:

StoreModule.provideStore( combineReducers([{
    project: projectReducer
}]) );

If i want to add other fields to my project, like a tags field:

{
    project: {
        name: string,
        tasks: Array<Task>,
        tags: Array<Tags>
    }
}

I can create a separate tags.reducer.ts and use the same approach to create the corresponding reducer.

So what's wrong with this approach ?

I'm pretty sure i'm running into troubles concerning the immutability of my application state.

Exemple:

  • I dispatch the CREATE_PROJECT action, i get a new state and everything is OK.
  • Then i dispatch a ADD_TASK action.
    • The tasksReducer itself return a brand new array of tasks, BUT the main application state is mutated ... And that is not good !

In your opinions, what's the best approach to solve this problem ?

More generally:

As my project object will grow bigger and bigger with more fields, how could i do to:

  • Keep reducer separated
  • Make reducer operate on isolated sub part of the 'main' reducer
  • Keep my state immutable

I would be glad to share though and opinions about that !

1

1 Answers

2
votes

First of all some remarks around your projectReducer. You are missing a default case in your switch statement, which is very important to have. (see http://blog.kwintenp.com/how-to-write-clean-reducers-and-test-them/#defaultcase). So add this to that reducer:

default:
   return state;

Secondly, you are doing something strange when calling your taskReducer. What you are doing when the projectReducer is called, is always calling your taskReducer. When in fact, you want to call the taskReducer if, and only if, it's an action he can do something with:

case 'ADD_TASK':
   // call it here

You are also overriding the current's state its task property. As you said, you are mutating your state, which is a definite thing to avoid. You can leverage the Object.assign operator to accomplish what you want like this (entire fix):

const projectReducer = (state:Project = null, action: Action): Project => {
    switch (action.type) {
        case 'CREATE_PROJECT':
            return {
                name :'New project',
                tasks: []
            };
        case 'ADD_TASK':
            return Object.assign({}, state, {tasks: tasksReducer( state.tasks, action )};
        default:
            return state;
    };
}

Now you are creating a new Project object when you are changing something to the tasks, which is what you want.

I would recommend you to read my entire blogpost for some pointers on how to write reducers in a clean way.

Your general questions could have an elaborated answer but that would skip the point I guess. Some short answers below.

Keep reducer separated

--> You are using the combineReducers helper method, which is a good thing to keep your reducers separated.

Make reducer operate on isolated sub part of the 'main' reducer

--> Same as above

Keep my state immutable

--> Follow the tips in my blogpost and use the Object.assign and the spread operator when needed.