Skip to content

feat: Hook for remote mutations #1450

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

Merged
merged 34 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1e2e10f
(wip) initial impl.
shuding Sep 12, 2021
8ba262a
fix test
shuding Sep 12, 2021
8383f03
(wip) fix deps
shuding Sep 12, 2021
bf9590f
merge master
shuding Sep 12, 2021
ac87cd0
initial implementation
shuding Sep 12, 2021
4a8adbf
fix linter
shuding Sep 12, 2021
4f9d67d
fix state reset
shuding Sep 12, 2021
9c52ce6
Merge branch 'master' into mutation
shuding Sep 13, 2021
b57bf8a
avoid reset race condition
shuding Sep 13, 2021
016cb47
fix race conditions
shuding Sep 13, 2021
8d5c28f
code tweaks
shuding Sep 13, 2021
3557505
code tweaks
shuding Sep 13, 2021
e485ae6
return bound mutate
shuding Sep 15, 2021
f047412
resolve conflicts
shuding Sep 23, 2021
d4a5ad3
apply review comments
shuding Sep 23, 2021
ad9212b
merge master
shuding Sep 30, 2021
1f22be9
resolve conflicts
shuding Oct 3, 2021
c3b621f
fix tsconfig
shuding Oct 3, 2021
e481922
type fixes
shuding Oct 4, 2021
67c9f36
fix lint errors
shuding Oct 4, 2021
c9fc40e
code tweaks
shuding Oct 4, 2021
dc79ba6
merge master
shuding Oct 13, 2021
9f31280
resolve conflicts
shuding Dec 28, 2021
4caa4b0
fix type error
shuding Dec 28, 2021
bedaddf
update types
shuding Dec 29, 2021
c7c1fc6
inline serialization result
shuding Jan 12, 2022
31850e6
merge main
shuding Apr 4, 2022
e55a83d
Merge branch 'main' into mutation
shuding Apr 11, 2022
2198dbe
merge main and update argument api
shuding Apr 11, 2022
cf0b795
add tests
shuding Apr 11, 2022
11546fa
fix tests
shuding Apr 11, 2022
94f3579
update typing
shuding Apr 11, 2022
be0d14a
update state api
shuding Apr 11, 2022
76d7864
change error condition
shuding Apr 11, 2022
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
10 changes: 6 additions & 4 deletions examples/axios-typescript/libs/useRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ export default function useRequest<Data = unknown, Error = unknown>(
request: GetRequest,
{ fallbackData, ...config }: Config<Data, Error> = {}
): Return<Data, Error> {
const { data: response, error, isValidating, mutate } = useSWR<
AxiosResponse<Data>,
AxiosError<Error>
>(
const {
data: response,
error,
isValidating,
mutate
} = useSWR<AxiosResponse<Data>, AxiosError<Error>>(
request && JSON.stringify(request),
/**
* NOTE: Typescript thinks `request` can be `null` here, but the fetcher
Expand Down
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module.exports = {
moduleNameMapper: {
'^swr$': '<rootDir>/src',
'^swr/infinite$': '<rootDir>/infinite/index.ts',
'^swr/immutable$': '<rootDir>/immutable/index.ts'
'^swr/immutable$': '<rootDir>/immutable/index.ts',
'^swr/mutation$': '<rootDir>/mutation/index.ts'
},
transform: {
'^.+\\.(t|j)sx?$': [
Expand Down
125 changes: 125 additions & 0 deletions mutation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 ditchMutationsTilRef = useRef(0)

const [stateRef, stateDependencies, setState] = useStateWithDeps(
{
data: UNDEFINED,
error: UNDEFINED,
isMutating: false
},
true
)
const currentState = stateRef.current

// Similar to the global mutate, but bound to the current cache and key.
// `cache` isn't allowed to change during the lifecycle.
const boundMutate = useCallback(
(arg0, arg1) => mutate(serialize(keyRef.current)[0], arg0, arg1),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)

const trigger = useCallback(async (extraArg, opts) => {
if (!fetcher) {
throw new Error('Can’t trigger the mutation: missing fetcher.')
}

const [serializedKey, args] = serialize(keyRef.current)

// 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()
ditchMutationsTilRef.current = mutationStartedAt

setState({ isMutating: true })
args.push(extraArg)

try {
const data = await mutate(
serializedKey,
(fetcher as any)(...args),
options
)

// If it's reset after the mutation, we don't broadcast any state change.
if (ditchMutationsTilRef.current <= mutationStartedAt) {
setState({ data, isMutating: false })
options.onSuccess && options.onSuccess(data, serializedKey, options)
}
return data
} catch (error) {
// If it's reset after the mutation, we don't broadcast any state change.
if (ditchMutationsTilRef.current <= mutationStartedAt) {
setState({ error: error as Error, isMutating: false })
options.onError && options.onError(error, serializedKey, options)
}
throw error
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const reset = useCallback(() => {
ditchMutationsTilRef.current = getTimestamp()
setState({ data: UNDEFINED, error: UNDEFINED, isMutating: false })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useIsomorphicLayoutEffect(() => {
keyRef.current = key
})

return {
mutate: boundMutate,
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 }
11 changes: 11 additions & 0 deletions mutation/package.json
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": "*"
}
}
8 changes: 8 additions & 0 deletions mutation/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./*.ts"]
}
79 changes: 79 additions & 0 deletions mutation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { SWRResponse, SWRConfiguration, Key, MutatorOptions } from 'swr'

type Async<Data> = Data | Promise<Data>

export type MutationFetcher<
Data = unknown,
SWRKey extends Key = Key,
ExtraArg = any
> =
/**
* () => [{ foo: string }, { bar: number }] | null
* () => ( [{ foo: string }, { bar: number } ] as const | null )
*/
SWRKey extends () => readonly [...infer Args] | null
? (...args: [...Args, ExtraArg]) => Async<Data>
: /**
* [{ foo: string }, { bar: number } ] | null
* [{ foo: string }, { bar: number } ] as const | null
*/
SWRKey extends readonly [...infer Args]
? (...args: [...Args, ExtraArg]) => Async<Data>
: /**
* () => string | null
* () => Record<any, any> | null
*/
SWRKey extends () => infer Arg | null
? (...args: [Arg, ExtraArg]) => Async<Data>
: /**
* string | null | Record<any,any>
*/
SWRKey extends null
? never
: SWRKey extends infer Arg
? (...args: [Arg, ExtraArg]) => Async<Data>
: never

export type SWRMutationConfiguration<
Data,
Error,
SWRMutationKey extends Key = Key,
ExtraArg = any
> = Pick<
SWRConfiguration<
Data,
Error,
MutationFetcher<Data, SWRMutationKey, ExtraArg>
>,
'fetcher' | 'onSuccess' | 'onError'
> &
MutatorOptions<Data>

export interface SWRMutationResponse<
Data = any,
Error = any,
SWRMutationKey extends Key = Key,
ExtraArg = any
> extends Omit<SWRResponse<Data, Error>, 'isValidating'> {
isMutating: boolean
trigger: (
extraArgument?: ExtraArg,
options?: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>
) => Promise<Data | undefined>
reset: () => void
}

export type SWRMutationHook = <
Data = any,
Error = any,
SWRMutationKey extends Key = Key,
ExtraArg = any
>(
...args:
| readonly [SWRMutationKey, MutationFetcher<Data, SWRMutationKey, ExtraArg>]
| readonly [
SWRMutationKey,
MutationFetcher<Data, SWRMutationKey, ExtraArg>,
SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg>
]
) => SWRMutationResponse<Data, Error, SWRMutationKey, ExtraArg>
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"import": "./immutable/dist/index.esm.js",
"require": "./immutable/dist/index.js",
"types": "./immutable/dist/immutable/index.d.ts"
},
"./mutation": {
"import": "./mutation/dist/index.esm.js",
"require": "./mutation/dist/index.js",
"types": "./mutation/dist/mutation/index.d.ts"
}
},
"react-native": "./dist/index.esm.js",
Expand All @@ -28,22 +33,26 @@
"dist/**",
"infinite/dist/**",
"immutable/dist/**",
"mutation/dist/**",
"infinite/package.json",
"immutable/package.json"
"immutable/package.json",
"mutation/package.json"
],
"repository": "vercel/swr",
"homepage": "https://swr.vercel.app",
"license": "MIT",
"scripts": {
"clean": "rimraf dist infinite/dist immutable/dist",
"build": "yarn build:core && yarn build:infinite && yarn build:immutable",
"clean": "rimraf dist infinite/dist immutable/dist mutation/dist",
"build": "yarn build:core && yarn build:infinite && yarn build:immutable && yarn build:mutation",
"watch": "yalc publish && node scripts/watch.js",
"watch:core": "node scripts/watch.js core",
"watch:infinite": "node scripts/watch.js infinite",
"watch:immutable": "node scripts/watch.js immutable",
"watch:mutation": "node scripts/watch.js mutation",
"build:core": "bunchee src/index.ts --no-sourcemap",
"build:infinite": "bunchee index.ts --cwd infinite --no-sourcemap",
"build:immutable": "bunchee index.ts --cwd immutable --no-sourcemap",
"build:mutation": "bunchee index.ts --cwd mutation --no-sourcemap",
"prepublishOnly": "yarn clean && yarn build",
"publish-beta": "yarn publish --tag beta",
"types:check": "tsc --noEmit --project tsconfig.check.json && tsc --noEmit -p test",
Expand Down
4 changes: 4 additions & 0 deletions scripts/watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const entryMap = {
immutable: {
entry: 'index.ts',
cwd: 'immutable'
},
mutation: {
entry: 'index.ts',
cwd: 'mutation'
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ export type MutatorOptions<Data = any> = {
rollbackOnError?: boolean
}

export type MutatorConfig = {
revalidate?: boolean
populateCache?: boolean
}

export type Broadcaster<Data = any, Error = any> = (
cache: Cache<Data>,
key: string,
Expand Down
13 changes: 5 additions & 8 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,11 @@ export const useSWRHandler = <Data = any, Error = any>(
}
const isValidating = resolveValidating()

const [stateRef, stateDependencies, setState] = useStateWithDeps<Data, Error>(
{
data,
error,
isValidating
},
unmountedRef
)
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data,
error,
isValidating
})

// The revalidation function is a carefully crafted wrapper of the original
// `fetcher`, to correctly handle the many edge cases.
Expand Down
16 changes: 10 additions & 6 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export const IS_SERVER = !hasWindow() || 'Deno' in window

// Polyfill requestAnimationFrame
export const rAF = (f: (...args: any[]) => void) =>
hasRequestAnimationFrame() ? window['requestAnimationFrame'](f) : setTimeout(f, 1)
hasRequestAnimationFrame()
? window['requestAnimationFrame'](f)
: setTimeout(f, 1)

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
Expand All @@ -15,12 +17,14 @@ export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect
// This assignment is to extend the Navigator type to use effectiveType.
const navigatorConnection =
typeof navigator !== 'undefined' &&
(navigator as Navigator & {
connection?: {
effectiveType: string
saveData: boolean
(
navigator as Navigator & {
connection?: {
effectiveType: string
saveData: boolean
}
}
}).connection
).connection

// Adjust the config based on slow connection status (<= 70Kbps).
export const slowConnection =
Expand Down
3 changes: 2 additions & 1 deletion src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export const mergeObjects = (a: any, b: any) => OBJECT.assign({}, a, b)
const STR_UNDEFINED = 'undefined'
export const hasWindow = () => typeof window != STR_UNDEFINED
export const hasDocument = () => typeof document != STR_UNDEFINED
export const hasRequestAnimationFrame = () => (hasWindow() && (typeof window['requestAnimationFrame'] != STR_UNDEFINED))
export const hasRequestAnimationFrame = () =>
hasWindow() && typeof window['requestAnimationFrame'] != STR_UNDEFINED
4 changes: 2 additions & 2 deletions src/utils/resolve-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useSWRConfig } from './use-swr-config'
// It's tricky to pass generic types as parameters, so we just directly override
// the types here.
export const withArgs = <SWRType>(hook: any) => {
return (((...args: any) => {
return ((...args: any) => {
// Get the default and inherited configuration.
const fallbackConfig = useSWRConfig()

Expand All @@ -25,5 +25,5 @@ export const withArgs = <SWRType>(hook: any) => {
}

return next(key, fn || config.fetcher, config)
}) as unknown) as SWRType
}) as unknown as SWRType
}
Loading