Skip to content
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

feat: Hook for remote mutations #1450

Merged
merged 34 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 32 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions examples/axios-typescript/libs/useRequest.ts
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
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?$': '@swc/jest'
Expand Down
125 changes: 125 additions & 0 deletions mutation/index.ts
@@ -0,0 +1,125 @@
import { useCallback, useRef } from 'react'
import useSWR, { useSWRConfig, Middleware, Key } from 'swr'

import { serialize } from '../src/utils/serialize'
huozhi marked this conversation as resolved.
Show resolved Hide resolved
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
},
true
)
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
shuding marked this conversation as resolved.
Show resolved Hide resolved
}
},
// 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 }
11 changes: 11 additions & 0 deletions mutation/package.json
@@ -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
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./*.ts"]
}
71 changes: 71 additions & 0 deletions mutation/types.ts
@@ -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>
21 changes: 16 additions & 5 deletions package.json
Expand Up @@ -31,36 +31,47 @@
"module": "./immutable/dist/index.esm.js",
"require": "./immutable/dist/index.js",
"types": "./immutable/dist/immutable/index.d.ts"
},
"./mutation": {
"import": "./mutation/dist/index.mjs",
"module": "./mutation/dist/index.esm.js",
"require": "./mutation/dist/index.js",
"types": "./mutation/dist/mutation/index.d.ts"
}
},
"types": "./dist/index.d.ts",
"files": [
"dist/**",
"infinite/dist/**",
"immutable/dist/**",
"mutation/dist/**",
"infinite/package.json",
"immutable/package.json"
"immutable/package.json",
"mutation/package.json"
],
"repository": "github: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",
"watch": "npm-run-all -p watch:core watch:infinite watch:immutable",
"clean": "rimraf dist infinite/dist immutable/dist mutation/dist",
"build": "yarn build:core && yarn build:infinite && yarn build:immutable && yarn build:mutation",
"watch": "npm-run-all -p watch:core watch:infinite watch:immutable watch:mutation",
"watch:core": "yarn build:core -w",
"watch:infinite": "yarn build:infinite -w",
"watch:immutable": "yarn build:immutable -w",
"watch:mutation": "yarn build:mutation -w",
"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",
"format": "prettier --write ./**/*.{ts,tsx}",
"lint": "eslint . --ext .ts,.tsx --cache",
"lint:fix": "yarn lint --fix",
"test": "jest --coverage"
"coverage": "jest --coverage",
"test": "jest"
},
"husky": {
"hooks": {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Expand Up @@ -145,6 +145,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
Expand Up @@ -131,14 +131,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
7 changes: 5 additions & 2 deletions src/utils/broadcast-state.ts
@@ -1,6 +1,7 @@
import { Broadcaster } from '../types'
import { SWRGlobalState, GlobalState } from './global-state'
import * as revalidateEvents from '../constants'
import { createCacheHelper } from './cache'

export const broadcastState: Broadcaster = (
cache,
Expand All @@ -15,6 +16,8 @@ export const broadcastState: Broadcaster = (
const revalidators = EVENT_REVALIDATORS[key]
const updaters = STATE_UPDATERS[key]

const [get] = createCacheHelper(cache, key)

// Cache was populated, update states of all hooks.
if (broadcast && updaters) {
for (let i = 0; i < updaters.length; ++i) {
Expand All @@ -30,10 +33,10 @@ export const broadcastState: Broadcaster = (

if (revalidators && revalidators[0]) {
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(
() => cache.get(key).data
() => get().data
)
}
}

return cache.get(key).data
return get().data
}
16 changes: 10 additions & 6 deletions src/utils/env.ts
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