forked from vercel/swr
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Hook for remote mutations (vercel#1450)
* (wip) initial impl. * fix test * (wip) fix deps * initial implementation * fix linter * fix state reset * avoid reset race condition * fix race conditions * code tweaks * code tweaks * return bound mutate * apply review comments * fix tsconfig * type fixes * fix lint errors * code tweaks * fix type error * update types * inline serialization result * merge main and update argument api * add tests * fix tests * update typing * update state api * change error condition
- Loading branch information
1 parent
ec83f43
commit 3797c65
Showing
17 changed files
with
1,159 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { useCallback, useRef } from 'react' | ||
import useSWR, { useSWRConfig, Middleware, Key } from 'swr' | ||
|
||
import { serialize } from '../src/utils/serialize' | ||
import { useStateWithDeps } from '../src/utils/state' | ||
import { withMiddleware } from '../src/utils/with-middleware' | ||
import { useIsomorphicLayoutEffect } from '../src/utils/env' | ||
import { UNDEFINED } from '../src/utils/helper' | ||
import { getTimestamp } from '../src/utils/timestamp' | ||
|
||
import { | ||
SWRMutationConfiguration, | ||
SWRMutationResponse, | ||
SWRMutationHook, | ||
MutationFetcher | ||
} from './types' | ||
|
||
const mutation = | ||
<Data, Error>() => | ||
( | ||
key: Key, | ||
fetcher: MutationFetcher<Data>, | ||
config: SWRMutationConfiguration<Data, Error> = {} | ||
) => { | ||
const { mutate } = useSWRConfig() | ||
|
||
const keyRef = useRef(key) | ||
// Ditch all mutation results that happened earlier than this timestamp. | ||
const ditchMutationsUntilRef = useRef(0) | ||
|
||
const [stateRef, stateDependencies, setState] = useStateWithDeps({ | ||
data: UNDEFINED, | ||
error: UNDEFINED, | ||
isMutating: false | ||
}) | ||
const currentState = stateRef.current | ||
|
||
const trigger = useCallback( | ||
async (arg, opts?: SWRMutationConfiguration<Data, Error>) => { | ||
const [serializedKey, resolvedKey] = serialize(keyRef.current) | ||
|
||
if (!fetcher) { | ||
throw new Error('Can’t trigger the mutation: missing fetcher.') | ||
} | ||
if (!serializedKey) { | ||
throw new Error('Can’t trigger the mutation: key isn’t ready.') | ||
} | ||
|
||
// Disable cache population by default. | ||
const options = Object.assign({ populateCache: false }, config, opts) | ||
|
||
// Trigger a mutation, also track the timestamp. Any mutation that happened | ||
// earlier this timestamp should be ignored. | ||
const mutationStartedAt = getTimestamp() | ||
|
||
ditchMutationsUntilRef.current = mutationStartedAt | ||
|
||
setState({ isMutating: true }) | ||
|
||
try { | ||
const data = await mutate<Data>( | ||
serializedKey, | ||
(fetcher as any)(resolvedKey, { arg }), | ||
options | ||
) | ||
|
||
// If it's reset after the mutation, we don't broadcast any state change. | ||
if (ditchMutationsUntilRef.current <= mutationStartedAt) { | ||
setState({ data, isMutating: false }) | ||
options.onSuccess?.(data as Data, serializedKey, options) | ||
} | ||
return data | ||
} catch (error) { | ||
// If it's reset after the mutation, we don't broadcast any state change. | ||
if (ditchMutationsUntilRef.current <= mutationStartedAt) { | ||
setState({ error: error as Error, isMutating: false }) | ||
options.onError?.(error as Error, serializedKey, options) | ||
throw error as Error | ||
} | ||
} | ||
}, | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[] | ||
) | ||
|
||
const reset = useCallback(() => { | ||
ditchMutationsUntilRef.current = getTimestamp() | ||
setState({ data: UNDEFINED, error: UNDEFINED, isMutating: false }) | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []) | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
keyRef.current = key | ||
}) | ||
|
||
// We don't return `mutate` here as it can be pretty confusing (e.g. people | ||
// calling `mutate` but they actually mean `trigger`). | ||
// And also, `mutate` relies on the useSWR hook to exist too. | ||
return { | ||
trigger, | ||
reset, | ||
get data() { | ||
stateDependencies.data = true | ||
return currentState.data | ||
}, | ||
get error() { | ||
stateDependencies.error = true | ||
return currentState.error | ||
}, | ||
get isMutating() { | ||
stateDependencies.isMutating = true | ||
return currentState.isMutating | ||
} | ||
} | ||
} | ||
|
||
export default withMiddleware( | ||
useSWR, | ||
mutation as unknown as Middleware | ||
) as unknown as SWRMutationHook | ||
|
||
export { SWRMutationConfiguration, SWRMutationResponse } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "swr-mutation", | ||
"version": "0.0.1", | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.esm.js", | ||
"types": "./dist/mutation", | ||
"peerDependencies": { | ||
"swr": "*", | ||
"react": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": "..", | ||
"outDir": "./dist" | ||
}, | ||
"include": ["./*.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { SWRResponse, Key, MutatorOptions } from 'swr' | ||
|
||
type FetcherResponse<Data> = Data | Promise<Data> | ||
|
||
type FetcherOptions<ExtraArg = unknown> = Readonly<{ | ||
arg: ExtraArg | ||
}> | ||
|
||
export type MutationFetcher< | ||
Data = unknown, | ||
ExtraArg = unknown, | ||
SWRKey extends Key = Key | ||
> = SWRKey extends () => infer Arg | null | undefined | false | ||
? (key: Arg, options: FetcherOptions<ExtraArg>) => FetcherResponse<Data> | ||
: SWRKey extends null | undefined | false | ||
? never | ||
: SWRKey extends infer Arg | ||
? (key: Arg, options: FetcherOptions<ExtraArg>) => FetcherResponse<Data> | ||
: never | ||
|
||
export type SWRMutationConfiguration< | ||
Data, | ||
Error, | ||
ExtraArg = any, | ||
SWRMutationKey extends Key = Key | ||
> = MutatorOptions<Data> & { | ||
fetcher?: MutationFetcher<Data, ExtraArg, SWRMutationKey> | ||
onSuccess?: ( | ||
data: Data, | ||
key: string, | ||
config: Readonly< | ||
SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg> | ||
> | ||
) => void | ||
onError?: ( | ||
err: Error, | ||
key: string, | ||
config: Readonly< | ||
SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg> | ||
> | ||
) => void | ||
} | ||
|
||
export interface SWRMutationResponse< | ||
Data = any, | ||
Error = any, | ||
ExtraArg = any, | ||
SWRMutationKey extends Key = Key | ||
> extends Pick<SWRResponse<Data, Error>, 'data' | 'error'> { | ||
isMutating: boolean | ||
trigger: ( | ||
extraArgument?: ExtraArg, | ||
options?: SWRMutationConfiguration<Data, Error, ExtraArg, SWRMutationKey> | ||
) => Promise<Data | undefined> | ||
reset: () => void | ||
} | ||
|
||
export type SWRMutationHook = < | ||
Data = any, | ||
Error = any, | ||
SWRMutationKey extends Key = Key, | ||
ExtraArg = any | ||
>( | ||
...args: | ||
| readonly [SWRMutationKey, MutationFetcher<Data, ExtraArg, SWRMutationKey>] | ||
| readonly [ | ||
SWRMutationKey, | ||
MutationFetcher<Data, ExtraArg, SWRMutationKey>, | ||
SWRMutationConfiguration<Data, Error, ExtraArg, SWRMutationKey> | ||
] | ||
) => SWRMutationResponse<Data, Error, ExtraArg, SWRMutationKey> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.