Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions gitbook/reactjrx/queries/react/usemutationusd.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
# useMutation$

## Run mutations one after another

Mutations run in parallel by default. Mutations that share a `scope.id` run one after another instead: each one starts once the one before it has succeeded or failed.

```tsx
const { mutate } = useMutation$({
scope: { id: "save-settings" },
mutationFn: (settings: Settings) => from(saveSettings(settings)),
})
```

A mutation settles on the first value its stream emits, so the next one starts then, even if the stream stays open.

`scope` is a TanStack Query option: the queue holds every mutation of the `QueryClient` with that id, whichever component started it. See [Mutation Scopes](https://tanstack.com/query/latest/docs/framework/react/guides/mutations#mutation-scopes).
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export * from "./lib/binding/useObservableState"
export * from "./lib/binding/useObserve"
export * from "./lib/binding/useSubscribe"
export * from "./lib/binding/useSubscribeEffect"
export * from "./lib/queries/useConcatMutation$"
export * from "./lib/queries/useLiveQueries$"
export * from "./lib/queries/useLiveQuery$"
export * from "./lib/queries/useMutation$"
Expand Down
7 changes: 3 additions & 4 deletions src/lib/queries/mutationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ type AdaptedMutationCallbacks<TData, TError, TVariables, TOnMutateResult> = {
}

/**
* Hooks built on top of `useMutation$` (`useSwitchMutation$`,
* `useConcatMutation$`) run their inner mutation with the user variables
* wrapped in an envelope (`{ variables, ... }`). This adapts the user-facing
* callbacks, which expect the raw variables, to that envelope.
* `useSwitchMutation$` runs its inner mutation with the user variables
* wrapped in an envelope (`{ variables, abort }`). This adapts the
* user-facing callbacks, which expect the raw variables, to that envelope.
*/
export function adaptCallbacksToWrappedVariables<
TData,
Expand Down
91 changes: 0 additions & 91 deletions src/lib/queries/useConcatMutation$.ts

This file was deleted.

64 changes: 62 additions & 2 deletions src/lib/queries/useMutation$.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { cleanup, render } from "@testing-library/react"
import {
act,
cleanup,
render,
renderHook,
waitFor,
} from "@testing-library/react"
import React, { useEffect } from "react"
import { finalize, ReplaySubject, takeUntil, timer } from "rxjs"
import { finalize, ReplaySubject, Subject, takeUntil, timer } from "rxjs"
import { afterEach, describe, expect, it } from "vitest"
import { createWrapper } from "../../tests/liveQuery"
import { waitForTimeout } from "../../tests/utils"
import { useMutation$ } from "./useMutation$"

Expand Down Expand Up @@ -69,4 +76,57 @@ describe("useMutation", () => {
})
})
})

describe("Given mutations that share a scope", () => {
it("should start each one once the previous one has failed or emitted", async () => {
const sources = {
first: new Subject<string>(),
second: new Subject<string>(),
third: new Subject<string>(),
}
const failure = new Error("first failed")
const started: string[] = []

const { result } = renderHook(
() =>
useMutation$({
scope: { id: "save" },
mutationFn: (name: keyof typeof sources) => {
started.push(name)

return sources[name]
},
}),
{ wrapper: createWrapper(new QueryClient()) },
)

let settled!: Promise<PromiseSettledResult<string>[]>

act(() => {
settled = Promise.allSettled([
result.current.mutateAsync("first"),
result.current.mutateAsync("second"),
result.current.mutateAsync("third"),
])
})

await waitFor(() => expect(started).toEqual(["first"]))

act(() => sources.first.error(failure))

await waitFor(() => expect(started).toEqual(["first", "second"]))

act(() => sources.second.next("second saved"))

await waitFor(() => expect(started).toEqual(["first", "second", "third"]))

act(() => sources.third.next("third saved"))

expect(await settled).toEqual([
{ status: "rejected", reason: failure },
{ status: "fulfilled", value: "second saved" },
{ status: "fulfilled", value: "third saved" },
])
})
})
})
Loading