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 12 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
4 changes: 2 additions & 2 deletions immutable/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import useSWR, { Middleware, SWRHook } from 'swr'
import useSWR, { Middleware } from 'swr'
import { withMiddleware } from '../src/utils/with-middleware'

export const immutable: Middleware = useSWRNext => (key, fetcher, config) => {
Expand All @@ -9,4 +9,4 @@ export const immutable: Middleware = useSWRNext => (key, fetcher, config) => {
return useSWRNext(key, fetcher, config)
}

export default withMiddleware(useSWR as SWRHook, immutable)
export default withMiddleware(useSWR, immutable)
7 changes: 4 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ 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'
},
globals: {
'ts-jest': {
tsconfig: 'test/tsconfig.json',
diagnostics: process.env.CI,
diagnostics: process.env.CI
}
},
}
}
115 changes: 115 additions & 0 deletions mutation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useCallback, useRef } from 'react'
import useSWR, { Middleware, Key, Fetcher, useSWRConfig } 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 { isUndefined, UNDEFINED } from '../src/utils/helper'
import { getTimestamp } from '../src/utils/timestamp'

import { SWRMutationConfiguration, SWRMutationResponse } from './types'

const mutation = <Data, Error>() => (
key: Key,
fetcher: Fetcher<Data>,
shuding marked this conversation as resolved.
Show resolved Hide resolved
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, Error>(
{
data: UNDEFINED,
error: UNDEFINED,
isValidating: false
},
true
)
const currentState = stateRef.current

const trigger = useCallback(async (extraArg, opts) => {
if (!fetcher) {
throw new Error('Can’t trigger the mutation: missing fetcher.')
shuding marked this conversation as resolved.
Show resolved Hide resolved
}

const [serializedKey, args] = serialize(keyRef.current)
const options = Object.assign({}, config, opts)

// Disable cache population by default.
if (isUndefined(options.populateCache)) {
options.populateCache = false
}
shuding marked this conversation as resolved.
Show resolved Hide resolved

// Trigger a mutation, also track the timestamp. Any mutation that happened
// earlier this timestamp should be ignored.
const mutationStartedAt = getTimestamp()
ditchMutationsTilRef.current = mutationStartedAt

try {
shuding marked this conversation as resolved.
Show resolved Hide resolved
setState({ isValidating: true })
args.push(extraArg)
const data = await mutate(
serializedKey,
fetcher.apply(UNDEFINED, args),
options
)
// If it's reset after the mutation, we don't broadcast any state change.
if (ditchMutationsTilRef.current <= mutationStartedAt) {
setState({ data, isValidating: 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, isValidating: false })
options.onError && options.onError(error, serializedKey, options)
}
throw error
}
}, [])
shuding marked this conversation as resolved.
Show resolved Hide resolved

const reset = useCallback(() => {
ditchMutationsTilRef.current = getTimestamp()
setState({ data: UNDEFINED, error: UNDEFINED, isValidating: false })
}, [])

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

return {
mutate,
shuding marked this conversation as resolved.
Show resolved Hide resolved
trigger,
reset,
get data() {
stateDependencies.data = true
return currentState.data
},
get error() {
stateDependencies.error = true
return currentState.error
},
get isMutating() {
stateDependencies.isValidating = true
return currentState.isValidating
}
} as SWRMutationResponse<Data, Error>
}

type SWRMutationHook = <Data = any, Error = any>(
...args:
| readonly [Key, Fetcher<Data>]
| readonly [Key, Fetcher<Data>, SWRMutationConfiguration<Data, Error>]
) => SWRMutationResponse<Data, Error>

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"]
}
19 changes: 19 additions & 0 deletions mutation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SWRResponse, SWRConfiguration, Fetcher } from 'swr'

export type SWRMutationConfiguration<Data, Error> = Pick<
SWRConfiguration<Data[], Error, Fetcher<Data[]>>,
'fetcher' | 'onSuccess' | 'onError'
> & {
revalidate?: boolean
populateCache?: boolean
}

export interface SWRMutationResponse<Data = any, Error = any>
extends Omit<SWRResponse<Data, Error>, 'isValidating'> {
isMutating: boolean
trigger: (
extraArgument?: any,
options?: SWRMutationConfiguration<Data, Error>
) => Promise<Data | undefined>
reset: () => void
}
17 changes: 13 additions & 4 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",
"watch": "yarn run-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": "yarn run-p watch:core watch:infinite watch:immutable watch:mutation",
"watch:core": "bunchee src/index.ts --watch",
"watch:infinite": "bunchee index.ts --cwd infinite --watch",
"watch:immutable": "bunchee index.ts --cwd immutable --watch",
"watch:mutation": "bunchee index.ts --cwd mutation --watch",
"build:core": "bunchee src/index.ts -m --no-sourcemap",
"build:infinite": "bunchee index.ts --cwd infinite -m --no-sourcemap",
"build:immutable": "bunchee index.ts --cwd immutable -m --no-sourcemap",
"build:mutation": "bunchee index.ts --cwd mutation -m --no-sourcemap",
"prepublishOnly": "yarn clean && yarn build",
"publish-beta": "yarn publish --tag beta",
"types:check": "tsc --noEmit",
Expand Down
7 changes: 4 additions & 3 deletions scripts/next-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ module.exports = {
// FIXME: resolving react/jsx-runtime https://github.com/facebook/react/issues/20235
alias['react/jsx-dev-runtime'] = require.resolve('react/jsx-dev-runtime.js')
alias['react/jsx-runtime'] = require.resolve('react/jsx-runtime.js')

alias['swr'] = resolve(__dirname, '../dist/index.js')
alias['swr/infinite'] = resolve(__dirname, '../infinite/dist/index.js')
alias['swr/immutable'] = resolve(__dirname, '../immutable/dist/index.js')
alias['swr/mutation'] = resolve(__dirname, '../mutation/dist/index.js')

alias['react'] = require.resolve('react')
alias['react-dom'] = require.resolve('react-dom')
alias['react-dom/server'] = require.resolve('react-dom/server')

return config
},
}
}
}
5 changes: 3 additions & 2 deletions scripts/require-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ const hookPropertyMap = new Map([
['swr', resolve(rootDir, 'dist/index.js')],
['swr/infinite', resolve(rootDir, 'infinite/dist/index.js')],
['swr/immutable', resolve(rootDir, 'immutable/dist/index.js')],
['swr/mutation', resolve(rootDir, 'mutation/dist/index.js')],
['react', resolve(nodeModulesDir, 'react')],
['react-dom', resolve(nodeModulesDir, 'react-dom')],
['react-dom', resolve(nodeModulesDir, 'react-dom')]
])

const resolveFilename = mod._resolveFilename
mod._resolveFilename = function (request, ...args) {
mod._resolveFilename = function(request, ...args) {
const hookResolved = hookPropertyMap.get(request)
if (hookResolved) request = hookResolved
return resolveFilename.call(mod, request, ...args)
Expand Down
18 changes: 12 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,19 @@ export type MutatorCallback<Data = any> = (
currentValue?: Data
) => Promise<undefined | Data> | undefined | Data

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

export type Broadcaster<Data = any, Error = any> = (
cache: Cache<Data>,
key: string,
data: Data,
error?: Error,
isValidating?: boolean,
shouldRevalidate?: boolean
shouldRevalidate?: boolean,
populateCache?: boolean
) => Promise<Data>

export type State<Data, Error> = {
Expand All @@ -114,27 +120,27 @@ export type Mutator<Data = any> = (
cache: Cache,
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorConfig
) => Promise<Data | undefined>

export interface ScopedMutator<Data = any> {
/** This is used for bound mutator */
(
key: Key,
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorConfig
): Promise<Data | undefined>
/** This is used for global mutator */
<T = any>(
key: Key,
data?: T | Promise<T> | MutatorCallback<T>,
shouldRevalidate?: boolean
opts?: boolean | MutatorConfig
): Promise<T | undefined>
}

export type KeyedMutator<Data> = (
data?: Data | Promise<Data> | MutatorCallback<Data>,
shouldRevalidate?: boolean
opts?: boolean | MutatorConfig
) => Promise<Data | undefined>

// Public types
Expand All @@ -147,7 +153,7 @@ export type SWRConfiguration<

export type Key = ValueKey | (() => ValueKey)

export interface SWRResponse<Data, Error> {
export interface SWRResponse<Data = any, Error = any> {
data?: Data
error?: Error
mutate: KeyedMutator<Data>
Expand Down
9 changes: 6 additions & 3 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ export const useSWRHandler = <Data = any, Error = any>(
data,
error,
isValidating
},
unmountedRef
}
)

// The revalidation function is a carefully crafted wrapper of the original
Expand Down Expand Up @@ -148,6 +147,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// start fetching
try {
cache.set(keyValidating, true)

setState({
isValidating: true
})
Expand Down Expand Up @@ -261,16 +261,19 @@ export const useSWRHandler = <Data = any, Error = any>(
}
} catch (err) {
cleanupState()

cache.set(keyValidating, false)

if (getConfig().isPaused()) {
setState({
isValidating: false
})
return false
}

// Get a new error, don't use deep comparison for errors.
// We don't use deep comparison for errors.
cache.set(keyErr, err)

if (stateRef.current.error !== err) {
// Keep the stale data but update error.
setState({
Expand Down
7 changes: 4 additions & 3 deletions src/utils/broadcast-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ export const broadcastState: Broadcaster = (
data,
error,
isValidating,
shouldRevalidate = false
shouldRevalidate,
populateCache = true
) => {
const [EVENT_REVALIDATORS, STATE_UPDATERS] = SWRGlobalState.get(
cache
) as GlobalState
const revalidators = EVENT_REVALIDATORS[key]
const updaters = STATE_UPDATERS[key]

// Always update states of all hooks.
if (updaters) {
// Cache was populated, update states of all hooks.
if (populateCache && updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](data, error, isValidating)
}
Expand Down
Loading