-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mutation results #317
Comments
Inserting a new object into an arrayLet's say we've got the following setup: fragment todoFields on Todo {
id
text
completed
}
query todoList {
todoList(id: 5) {
id
todos {
...todoFields
}
}
}
mutation createTodo {
createTodo(todo: {...}) {
todo {
...todoFields
}
}
} Essentially, we are looking at a todo list, and we want to create an item in that list. Let's say we want the item to show up at the top. To do this we need the following information:
The most basic way to provide that would be:
So a preliminary sketch of the API would be something like: applyResult: [{
type: 'ARRAY_INSERT',
resultPath: [ 'createTodo', 'todo' ],
storePath: [ '5', 'todos' ],
where: 'prepend',
}] This seems pretty good to start. Some concerns:
I'd say we can safely ignore (1) to start with, and ship that separately as something different later. For (2), we'll just have to see when we get there. |
Delete an objectLet's add another mutation: mutation deleteTodo {
deleteTodo(todoId: 10) {
id
}
} I think it is pretty reasonable that the object should have an ID if it is to be deleted. So we can just have the mutations return the ID of the deleted item. Since we might have custom dataId generation, we need all of the fields used by that method in the response. In this case, we probably need:
So we could, say, do the following: applyResult: [{
type: 'DELETE',
dataIdToDelete: (result) => 'Todo' + result.deleteTodo.id,
}] One concern is that we are hard-coding the typename here, and replicating the logic for getting the dataId. Perhaps we can get the deleteTodo: (dataId, id) => ({
mutation: gql`...`,
applyResult: [{
type: 'DELETE',
dataId,
}]
}) This would imply that it's helpful to have a way to get that from the query, perhaps: fragment todoFields on Todo {
__apolloDataId
id
text
completed
} Where Questions about this approach:
|
Remove an object from an arrayThis would happen if we had something that moved the todo to a different list: moveTodo: (dataId, todoId, targetListId) => ({
mutation: gql`...`,
applyResult: [{
type: 'ARRAY_REMOVE',
dataId,
storePath: [ '5', 'todos' ],
}]
}) This is basically like a cross between I don't think there are any new questions over the above. |
Writing a custom reducerWe should document well the implementation of the above methods, which are basically like mini reducers. Maybe you can add new ones via the apollo client constructor? const client = new ApolloClient({
...,
mutationResultReducers: {
MULTI_INSERT: multiInsertReducer,
}
}); So this would be like: // state is the 'data' section of the store state
// action is just the 'applyResult' array item
function multiInsertReducer(state, action, result) {
const array = readFragmentFromStore(...);
// add result items to array, based on action data
return writeFragmentToStore(state, ...);
} Seems like if we document the tools well it will be quite possible to write nice custom reducers. |
What if Client queries -> query results hit our custom reducer maybe specified via Then we can also mutate the data via this custom reducer. This would actually help Optimistic UI, client state mgmt, etc. |
Sounds pretty interesting! I'm sure the design for mutations can transfer over to that as well |
The more I think about this, the more I'm contemplating a In redux, our reducers describe our state change. So whether its a query or a mutation, a reducer is expecting some shape of data to then return a new state. If we take this approach with Maybe it can work like this,
I guess the question then is, why do we store everything under a data object today? Do we need to do this? If these custom reducers represent the slices of state from our server, we can associate queries to them, hydrate them on the server, etc. We can also unit test these specific pieces of state in isolation? |
What do we gain by having a store path? moveTodo: (dataId, todoId, targetListId) => ({
mutation: gql`...`,
applyResult: [{
type: 'ARRAY_REMOVE',
dataId,
storePath: [ '5', 'todos' ],
}]
}) Potential view from query fetchTodos: () => ({
query: gql`...`,
// allTodos represent the reducer we associate the state change to
stateKey: 'allTodos',
applyResult: [{
type: 'ARRAY_INIT',
}]
}) |
@abhiaiyer91 we need store path to tell Apollo which array we are trying to remove from. The goal of this action is if you have an item that is displayed in one or more lists on the page, and you want to stop displaying it there. This is for cases where you aren't deleting the item entirely, so we can't just remove all references like we do with For the query example, is this better than doing |
Merged #320 |
adding a super simple client/server example
There needs to be a good way to incorporate the result of a mutation into the store. You can already use the result of a mutation to do the following:
then
of the promisethen
of the promiseI think we need a few more, informed by talking to users and also by looking at the options for Relay mutation configs:
Lastly, there needs to be an escape hatch, which perhaps will require a lot of documentation to get right, but is important so that people don't feel like they are limited.
writeFragmentToStore
to do anything you like with the result.The text was updated successfully, but these errors were encountered: