I'm building a simple todo app where a user can create projects and then add todo items. I believe my state should look something like this:
{
projects: {
1: {
id: 1,
title: "New Project",
todos: [1, 2]
}
},
todos: {
1: {
id: 1,
text: "This is the first todo",
isComplete: true,
project: 1
},
2: {
id: 2,
text: "This is the second todo",
isComplete: false,
project: 1
}
}
}
When creating a new todo I need to update the todos
state with the new todo and I need to update the parent project in the projects
state.
What is the best way to handle this? Do both of the reducers need actions to handle this? Or is there someway the todos
reducer can call an update action in the projects
reducer?
EDIT: Here's how I changed the data structure to work better with redux
{
projects: {
condition: {
currentProject: 1
},
entities: {
1: {
id: 1,
title: "New Project"
}
}
},
todos: {
condition: {
currentFilter: 'SHOW_ALL'
},
entities: {
1: {
id: 1,
text: "This is the first todo",
isComplete: true,
project: 1
},
2: {
id: 2,
text: "This is the second todo",
isComplete: false,
project: 1
}
}
}
}
This way each of my reducers being combined at the root level are scoped to one root key. entities
are persisted but condition
is not. All other parts of state are computed use reselect
. I'd be curious to see how others are solving similar problems in a front end only app.
When projects reference their todos and vice versa, you're duplicating data. Unless you're working on an extremely large collection, I would recommend against that pattern. Instead, whenever you need a list of todos that match a project, filter the todos collection. Something like reselect
is ideal for this.
This way when you dispatch CREATE_TODO
, you only need the todos
reducer to listen.
Keep your reducers decoupled.
Dispatching actions in reducers is an anti-pattern. You can't do something in one reducer and pass that information to another directly. A better solution is
dispatch({ type: CREATE_TODO, payload: { text: 'dsfdf' })
dispatch({ type: ASSOCIATE_TODO_WITH_PROJECT, payload: { todo: todoid, project: 2 })
but even that won't work because you don't know what todoid
is. Which brings me to...
You shouldn't have to know the id of the todo before you create it. In your reducer setup, you need to know the todo's id in order to add it to a project. That means you need to provide it in the action payload. But then you're view would have to generate the next id is, which is not ideal.
Calling state.todos.filter(todo => todo.project == currentProject.id)
in mapStateToProps
is a better solution than figuring out what the next id should be in the view and passing it with the action.
If this is done with a db, then you would use something like this with redux-thunk
export function createTodo(text, project) {
return function(dispatch) {
dispatch({ type: CREATE_TODO_PENDING });
fetch('/todos', { method: 'POST', body: { text: text, project: project } })
.then(function(response) { // { id: 134452, text: text, project: project }
dispatch({ type: CREATE_TODO_SUCCESS, payload: response })
}.catch(err) { dispatch({ type: CREATE_TODO_FAIL }) }
}
}
If you're using a backend that handles the id, then you can do the above and have both reducers listen to the events and assign todos to projects and projects to todos at the same time. But I still don't really recommend doing that.
You risk having data out of sync because you lack a single source of truth. If your projects reducer says a project owns a todo with id === 3
, but that todo says a different project owns it, who does that todo belong to? Obviously a bug has occured, but that could be prevented altogether by only having one child keep track of parent.
Though, again, if you're using a database, then the database should keep those things synced up (still more surface area for bugs, but less likely). In which case as long as your client data always reflected server data, you should be fine.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With