0

I will use the following popular todo list example for demonstration. Let's say I wanted to create a very basic todo list with this structure as the app state:

{
    todos: {
        todoKey1: {
            content: "todoKey1 content",
            ...
        },
        todoKey2: ...
    }
}

Because of the recursive nature of reducers, we can separate out the reducers into different files:

// reducers/app.js:
import todos from "./todos";
export default function app(prev={}, action){
    return {
        todos: todos(prev.todos, action)
    };
}

// reducers/todos.js:
import todo from "./todo";
export default function todos(prev={}, action){
    switch(action.type){
        case "ADD_TODO":
            return {
                ...prev,
                [action.id]: todo(undefined, action)
            };
        default:
            return prev;
    }
}

// reducers/todo.js:
export default function todo(prev={}, action){
    switch(action.type){
        case "ADD_TODO":
            return {content: action.content};
        default:
            return prev;
    }
}

Everything looks good so far - nice and organized. This is where my question comes in: I don't want to include the new key for the new todo in the action. Instead I want the action to be very pure, in which it should describe only the action (adding a new todo item). The id is irrelevant.

I would like the key to be automatically generated whenever I add a todo item and thus I have modified the state structure to include next_available_id which indicates what the key should the next todo item be using:

{
    next_available_id: 0,
    todos: {
        todoKey1: {
            content: "todoKey1 content",
            ...
        },
        todoKey2: ...
    }
}

This breaks the nice recursive reducers that I had since now it todos() doesn't have the id to create the new entry and the action does not have the id. Instead, I will have to combine the reducer in todos.js into app.js:

// reducers/app.js:
import todo from "./todo";
export default function app(prev={}, action){
    switch(action.type){
        case "ADD_TODO":
            let todos = prev.todos || {};
            return {
                ...prev,
                next_available_id: prev.next_available_id + 1,
                todos: {
                    ...todos,
                    [prev.next_available_id]: todo(undefined, action)
                }
            };
        default:
            return prev;
    }
}

Possible solutions that I can think of:

  • Dispatch multiple actions: one for incrementing next_available_id and another one for the actual todo creation.
    • Pro: app.js and todos.js can remain separated
    • Con: This can get really messy in the actual application as there would be a lot of extra actions
  • Modify the action itself before passing into todos.
    • Pro: Eliminates the need for an additional action
    • Con: Actions should be immutable

I feel like I'm missing something here. What would be the standard way of handling situations like this?

0

I have come up with a solution where I simply don't consider the next available id as part of the app state.

In my actions.js, I have added a counter that simply increments whenever addTodo is called:

let next_available_id = 0;
export function addTodo(content) {
    return {
        id: next_available_id++,
        content
    };
}

This works perfectly fine and I am now able to separate the reducers into different files once again. Not sure how "correct" this is since this probably introduces side effects, but it this case it doesn't really matter.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.