Replies: 6 comments 18 replies
-
For convenience, we should allow short-circuiting the promise with the value, so the signature would look like: export function createMutation<T, U>(
mutation: (variables: T) => Promise<U> | U
options?: {
onSuccess?: (data: U, done: () => void) => void
onError?: (error: unknown, done: () => void) => void
}
): [() => T | undefined, (variables: T) => Promise<U>] Also, this is basically an async memo. We should at least document that this can be an intended use apart from mutation for resources. Lastly, will the use of an |
Beta Was this translation helpful? Give feedback.
-
Ah, I misunderstood the first part of the return value. In this case the added value is basically more control over the mutation of resources? |
Beta Was this translation helpful? Give feedback.
-
I don't have an opinion overall, because generally I've wired my own logic directly to signals and that's about it (i may be missing out and should start trying out these higher level patterns, but things seem to be getting a little complicated this far away from signals). But, what I noticed is it uses callbacks with async done callbacks passed in (if I understand correctly). Should we strive to make everything as signal-based as possible, rather than using callbacks? Perhaps @davedbase may have an idea here. I don't know exactly what problem this primitive solves (can you show an example?), but maybe this shows more of the signals-for-everything idea: const [adding, newTodo, error] = createMutation(sendAddTodo);
createEffect(() => {
mutate(todos => [...todos, newTodo()]);
}) The reason for this idea is that callback patterns prevent composability. With signals, we can do this: const [adding, a, errorA] = createMutation(sendA);
const [adding, b, errorB] = createMutation(sendB);
createEffect(() => {
if (a() && b())
mutate(things => [...things, a()+b()])
else if (errorA() || errorB()) {...}
}) I write all this without understanding what createMutation is for, but with understanding of data being passed as callbacks vs signals. If you could provide a code example of what is difficult and what createMutation would then solve, that'd be helpful. |
Beta Was this translation helpful? Give feedback.
-
I like the ideas put here, but I think we can make it more low level to work for any Promise, and use signals for the promise result. function createPromisePrimitive<T, U>(
src: (variables: T) => Promise<U>,
options: {
onSuccess: (data: U, done: () => void) => void,
onError: (error: unknown, done: () => void) => void
}
): [
() => boolean, // inflight
{
invoke: (variables: T) => Promise<U>, // invokes the promise when not already inflight
abort: () => void // aborts the current promise if inflight
}
] |
Beta Was this translation helpful? Give feedback.
-
So how does this work if there are 2 mutations in flight? (for the same mutator) |
Beta Was this translation helpful? Give feedback.
-
Is work still being done one this? I think that the approach in createRouteAction with action.input is pretty good, but it still requires a good deal of logic. If solidstart (or solidjs) did something like https://swr.vercel.app/blog/swr-v2.en-US#optimistic-ui, where createRouteData/createResource could return an action method (similar to the mutate method with optimisticData and rollbackOnError as parameters, the amount of logic and mental overhead would be greatly reduced. (When I was first learning about optimistic UI from the solid start docs, I was surprised at first that the apps I use daily do this much logic to get optimistic updates, and I thought it should work by modifying the actual data hook) That being said, we would not get the grayed-out pending indicators like in Ryan's demo, so it would be important to keep both options. I would be happy to work on this if necessary and continue the discussion in the solidstart repo if necessary. |
Beta Was this translation helpful? Give feedback.
-
Summary
In this RFC I'm looking for feedback on the potential of a core Mutation primitive. While nothing being proposed could not be done in user-land, there is a good argument for establishing a simple pattern as sort of the complement of
createResource
. Resources do async calls, but are all about the data reads. Mutations are innately writes and actually don't really fit the pattern.Background
Libraries like React Query or Apollo have
useMutation
hooks in React to along with theiruseQuery
. These solutions are much more involved where Solid'screateResource
is more of a building block. However they establish a good pattern to imitate. One observation is there returned data is generally the last successful results.However, another influence is Remix's forms. They expose your in-flight request as state as a way to allow for optimistic updates for the life of the request. This seems much more useful.
Proposal
Solid will have a new mutation primitive
createMutation
.createMutation
The primitive will roughly carry this signature
The mutation is the method that takes variables and returns the promise for the mutation. The options object has callbacks for success and error. Important beyond passing data/error they have a done callback. If the arity of the function is 1 the caller will assume that the whole thing is done immediately, but if 2, done can be called at your discretion to complete the mutation. This is important to keep the optimistic updates in place say when refetching a resource.
The first return value is the infight signal which returns the submitted vars while the mutation is executing and undefined otherwise. This is useful for optimistic UIs. The second is the actual function that triggers the mutation.
Using with Resources
Most of the time mutations will be used with resources. In the success callback you will either take the data and apply the change manually in the browser, or tell the resource/s to refetch. Keep in mind if you don't want Suspense to fallback use Transitions when refetching.
Optimistic Updates
Optimistic updates are one of the biggest benefits of this approach. There are lots of things you can do from treating the presence of inflight data as a pending indicator or rendering the update as if it is already there. Here is a rudimentary implementation and example of some of the things that can be done with this approach:
https://codesandbox.io/s/solid-todos-mutation-flmh6?file=/index.tsx
UPDATED: Here is another alternative example that handles multiple inflight:
https://codesandbox.io/s/solid-todos-mutation-v2-llequ?file=/src/index.tsx
Impact
This is a new feature with no proposed deprecations. However, this has the potential to be the foundation for other primitives so it is really important that we get the API right. So please leave your thoughts and feeback.
Beta Was this translation helpful? Give feedback.
All reactions