use-context-selector
Advanced tools
Comparing version 2.0.0-alpha.9 to 2.0.0-beta.0
106
package.json
{ | ||
"name": "use-context-selector", | ||
"description": "React useContext with selector support in userland", | ||
"version": "2.0.0-alpha.9", | ||
"description": "React useContextSelector hook in userland", | ||
"version": "2.0.0-beta.0", | ||
"publishConfig": { | ||
"tag": "next" | ||
}, | ||
"type": "module", | ||
"author": "Daishi Kato", | ||
@@ -14,5 +15,17 @@ "repository": { | ||
"source": "./src/index.ts", | ||
"main": "./dist/index.umd.js", | ||
"module": "./dist/index.modern.js", | ||
"types": "./dist/src/index.d.ts", | ||
"main": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"exports": { | ||
"./package.json": "./package.json", | ||
".": { | ||
"require": { | ||
"types": "./dist/cjs/index.d.ts", | ||
"default": "./dist/cjs/index.js" | ||
}, | ||
"default": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
} | ||
} | ||
}, | ||
"sideEffects": false, | ||
@@ -23,11 +36,17 @@ "files": [ | ||
], | ||
"packageManager": "pnpm@8.15.0", | ||
"scripts": { | ||
"compile": "microbundle build -f modern,umd", | ||
"test": "run-s eslint tsc-test jest", | ||
"eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .", | ||
"jest": "jest --preset ts-jest/presets/js-with-ts __tests__/*.tsx", | ||
"tsc-test": "tsc --project . --noEmit", | ||
"apidoc": "documentation readme --section API --markdown-toc false --parse-extension ts src/*.ts", | ||
"examples:01_minimal": "DIR=01_minimal EXT=js webpack-dev-server", | ||
"examples:02_typescript": "DIR=02_typescript webpack-dev-server" | ||
"compile": "rm -rf dist && pnpm run '/^compile:.*/'", | ||
"compile:esm": "tsc -p tsconfig.esm.json", | ||
"compile:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", | ||
"test": "pnpm run '/^test:.*/'", | ||
"test:format": "prettier -c .", | ||
"test:lint": "eslint .", | ||
"test:types": "tsc -p . --noEmit", | ||
"test:types:examples": "tsc -p examples --noEmit", | ||
"test:spec": "vitest run", | ||
"apidoc": "documentation readme src/index.ts --section API --markdown-toc false --parse-extension ts --require-extension ts", | ||
"examples:01_counter": "DIR=01_counter vite", | ||
"examples:02_person": "DIR=02_person vite", | ||
"examples:03_suspense": "DIR=03_suspense vite" | ||
}, | ||
@@ -40,36 +59,37 @@ "keywords": [ | ||
"license": "MIT", | ||
"dependencies": {}, | ||
"prettier": { | ||
"singleQuote": true | ||
}, | ||
"devDependencies": { | ||
"@testing-library/react": "^10.4.7", | ||
"@types/jest": "^26.0.8", | ||
"@types/react": "^16.9.44", | ||
"@types/react-dom": "^16.9.8", | ||
"@types/scheduler": "^0.16.1", | ||
"@typescript-eslint/eslint-plugin": "^3.7.1", | ||
"@typescript-eslint/parser": "^3.7.1", | ||
"documentation": "^13.0.2", | ||
"eslint": "^7.6.0", | ||
"eslint-config-airbnb": "^18.2.0", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-jsx-a11y": "^6.3.1", | ||
"eslint-plugin-react": "^7.20.5", | ||
"eslint-plugin-react-hooks": "^4.0.8", | ||
"html-webpack-plugin": "^4.3.0", | ||
"jest": "^26.2.2", | ||
"microbundle": "^0.12.3", | ||
"npm-run-all": "^4.1.5", | ||
"react": "experimental", | ||
"react-dom": "experimental", | ||
"scheduler": "experimental", | ||
"ts-jest": "^26.1.4", | ||
"ts-loader": "^8.0.2", | ||
"typescript": "^3.9.7", | ||
"webpack": "^4.44.1", | ||
"webpack-cli": "^3.3.12", | ||
"webpack-dev-server": "^3.11.0" | ||
"@testing-library/jest-dom": "^6.4.2", | ||
"@testing-library/react": "^15.0.5", | ||
"@testing-library/user-event": "^14.5.2", | ||
"@types/node": "^20.12.7", | ||
"@types/react": "^18.3.1", | ||
"@types/react-dom": "^18.3.0", | ||
"@types/scheduler": "^0.23.0", | ||
"@typescript-eslint/eslint-plugin": "^7.7.1", | ||
"@typescript-eslint/parser": "^7.7.1", | ||
"documentation": "^14.0.3", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-import-resolver-typescript": "^3.6.1", | ||
"eslint-plugin-import": "^2.29.1", | ||
"eslint-plugin-jsx-a11y": "^6.8.0", | ||
"eslint-plugin-react": "^7.34.1", | ||
"eslint-plugin-react-hooks": "^4.6.2", | ||
"happy-dom": "^14.7.1", | ||
"prettier": "^3.2.5", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
"scheduler": "^0.23.2", | ||
"ts-expect": "^1.3.0", | ||
"typescript": "^5.4.5", | ||
"vite": "^5.2.10", | ||
"vitest": "^1.5.2" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=16.14.0", | ||
"scheduler": ">=0.20.0" | ||
"react": ">=18.0.0", | ||
"scheduler": ">=0.19.0" | ||
} | ||
} |
189
README.md
# use-context-selector | ||
[![CI](https://img.shields.io/github/workflow/status/dai-shi/use-context-selector/CI)](https://github.com/dai-shi/use-context-selector/actions?query=workflow%3ACI) | ||
[![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/use-context-selector/ci.yml?branch=main)](https://github.com/dai-shi/use-context-selector/actions?query=workflow%3ACI) | ||
[![npm](https://img.shields.io/npm/v/use-context-selector)](https://www.npmjs.com/package/use-context-selector) | ||
[![size](https://img.shields.io/bundlephobia/minzip/use-context-selector)](https://bundlephobia.com/result?p=use-context-selector) | ||
[![discord](https://img.shields.io/discord/627656437971288081)](https://discord.gg/MrQdmzd) | ||
React useContext with selector support in userland | ||
React useContextSelector hook in userland | ||
@@ -23,16 +24,31 @@ ## Introduction | ||
v1 uses `calculateChangedBits=()=>0` technique to stop propagation, | ||
while v2 uses `useMutableSource`. | ||
Prior to v1.3, it uses `changedBits=0` feature to stop propagation, | ||
v1.3 no longer depends on this undocumented feature. | ||
## Install | ||
This package requires some peer dependencies, which you need to install by yourself. | ||
```bash | ||
npm install use-context-selector | ||
yarn add use-context-selector react scheduler | ||
``` | ||
Notes for library authors: | ||
Please do not forget to keep `"peerDependencies"` and | ||
note instructions to let users to install peer dependencies. | ||
## Technical memo | ||
To make it work like original React context, it uses | ||
[useReducer cheat mode](https://overreacted.io/a-complete-guide-to-useeffect/#why-usereducer-is-the-cheat-mode-of-hooks) intentionally. | ||
It also requires `useContextUpdate` to behave better in concurrent rendering. | ||
Its usage is optional and only required if the default behavior is unexpected. | ||
## Usage | ||
```javascript | ||
import React, { useState } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { useState } from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
@@ -44,12 +60,15 @@ import { createContext, useContextSelector } from 'use-context-selector'; | ||
const Counter1 = () => { | ||
const count1 = useContextSelector(context, v => v[0].count1); | ||
const setState = useContextSelector(context, v => v[1]); | ||
const increment = () => setState(s => ({ | ||
...s, | ||
count1: s.count1 + 1, | ||
})); | ||
const count1 = useContextSelector(context, (v) => v[0].count1); | ||
const setState = useContextSelector(context, (v) => v[1]); | ||
const increment = () => | ||
setState((s) => ({ | ||
...s, | ||
count1: s.count1 + 1, | ||
})); | ||
return ( | ||
<div> | ||
<span>Count1: {count1}</span> | ||
<button type="button" onClick={increment}>+1</button> | ||
<button type="button" onClick={increment}> | ||
+1 | ||
</button> | ||
{Math.random()} | ||
@@ -61,12 +80,15 @@ </div> | ||
const Counter2 = () => { | ||
const count2 = useContextSelector(context, v => v[0].count2); | ||
const setState = useContextSelector(context, v => v[1]); | ||
const increment = () => setState(s => ({ | ||
...s, | ||
count2: s.count2 + 1, | ||
})); | ||
const count2 = useContextSelector(context, (v) => v[0].count2); | ||
const setState = useContextSelector(context, (v) => v[1]); | ||
const increment = () => | ||
setState((s) => ({ | ||
...s, | ||
count2: s.count2 + 1, | ||
})); | ||
return ( | ||
<div> | ||
<span>Count2: {count2}</span> | ||
<button type="button" onClick={increment}>+1</button> | ||
<button type="button" onClick={increment}> | ||
+1 | ||
</button> | ||
{Math.random()} | ||
@@ -77,10 +99,7 @@ </div> | ||
const StateProvider = ({ children }) => { | ||
const [state, setState] = useState({ count1: 0, count2: 0 }); | ||
return ( | ||
<context.Provider value={[state, setState]}> | ||
{children} | ||
</context.Provider> | ||
); | ||
}; | ||
const StateProvider = ({ children }) => ( | ||
<context.Provider value={useState({ count1: 0, count2: 0 })}> | ||
{children} | ||
</context.Provider> | ||
); | ||
@@ -94,21 +113,5 @@ const App = () => ( | ||
ReactDOM.render(<App />, document.getElementById('app')); | ||
createRoot(document.getElementById('app')).render(<App />); | ||
``` | ||
## Migrating from v1 to v2 | ||
In v1: | ||
```js | ||
useContextSelector(context, state => state.count); | ||
``` | ||
In v2: | ||
```js | ||
useContext(context, useCallback(state => state.count, [])); | ||
``` | ||
In this case, you can (should) also define the selector function outside render. | ||
## API | ||
@@ -120,11 +123,7 @@ | ||
This creates a special context for selector-enabled `useContext`. | ||
This creates a special context for `useContextSelector`. | ||
It doesn't pass its value but a ref of the value. | ||
Unlike the original context provider, this context provider | ||
expects the context value to be immutable and stable. | ||
#### Parameters | ||
- `defaultValue` **Value** | ||
* `defaultValue` **Value**  | ||
@@ -139,10 +138,8 @@ #### Examples | ||
### useContext | ||
### useContextSelector | ||
This hook returns context value with optional selector. | ||
This hook returns context selected value by selector. | ||
It will only accept context created by `createContext`. | ||
It will trigger re-render if only the selected value is referentially changed. | ||
The selector must be stable. | ||
Either define selector outside render or wrap with `useCallback`. | ||
@@ -153,4 +150,4 @@ The selector should return referentially equal result for same input for better performance. | ||
- `context` **Context<Value>** | ||
- `selector` **function (value: Value): Selected** (optional, default `identity as(value:Value)=>Selected`) | ||
* `context` **Context\<Value>**  | ||
* `selector` **function (value: Value): Selected**  | ||
@@ -160,5 +157,22 @@ #### Examples | ||
```javascript | ||
import { useContextSelector } from 'use-context-selector'; | ||
const firstName = useContextSelector(PersonContext, (state) => state.firstName); | ||
``` | ||
### useContext | ||
This hook returns the entire context value. | ||
Use this instead of React.useContext for consistent behavior. | ||
#### Parameters | ||
* `context` **Context\<Value>**  | ||
#### Examples | ||
```javascript | ||
import { useContext } from 'use-context-selector'; | ||
const firstName = useContext(PersonContext, state => state.firstName); | ||
const person = useContext(PersonContext); | ||
``` | ||
@@ -168,9 +182,11 @@ | ||
This hook returns an update function that accepts a thunk function | ||
This hook returns an update function to wrap an updating function | ||
Use this for a function that will change a value. | ||
Use this for a function that will change a value in | ||
concurrent rendering in React 18. | ||
Otherwise, there's no need to use this hook. | ||
#### Parameters | ||
- `context` **Context<any>** | ||
* `context` **Context\<Value>**  | ||
@@ -183,3 +199,8 @@ #### Examples | ||
const update = useContextUpdate(); | ||
// Wrap set state function | ||
update(() => setState(...)); | ||
// Experimental suspense mode | ||
update(() => setState(...), { suspense: true }); | ||
``` | ||
@@ -191,15 +212,14 @@ | ||
Type: React.FC<{context: Context<any>, value: any}> | ||
#### Parameters | ||
- `$0` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** | ||
- `$0.context` | ||
- `$0.value` | ||
- `$0.children` | ||
* `$0` **{context: Context\<any>, value: any, children: ReactNode}**  | ||
* `$0.context`   | ||
* `$0.value`   | ||
* `$0.children`   | ||
#### Examples | ||
```javascript | ||
const valueToBridge = useContext(PersonContext); | ||
const valueToBridge = useBridgeValue(PersonContext); | ||
return ( | ||
@@ -214,8 +234,17 @@ <Renderer> | ||
### useBridgeValue | ||
This hook return a value for BridgeProvider | ||
#### Parameters | ||
* `context` **Context\<any>**  | ||
## Limitations | ||
- In order to stop propagation, `children` of a context provider has to be either created outside of the provider or memoized with `React.memo`. | ||
- Neither context consumers or class components are supported. | ||
- The [stale props](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) issue can't be solved in userland. | ||
- Tearing is only avoided within the Provider tree. A value outside the Provider will tear. (`02_tearing_spec` fails) | ||
* In order to stop propagation, `children` of a context provider has to be either created outside of the provider or memoized with `React.memo`. | ||
* Provider trigger re-renders only if the context value is referentially changed. | ||
* Neither context consumers or class components are supported. | ||
* The [stale props](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) issue can't be solved in userland. | ||
* Tearing is only avoided if all consumers get data using `useContextSelector`. If you use both props and `use-context-selector` to pass the same data, they may provide inconsistence data for a brief moment. (`02_tearing_spec` fails) | ||
@@ -228,3 +257,3 @@ ## Examples | ||
```bash | ||
PORT=8080 npm run examples:01_minimal | ||
PORT=8080 yarn run examples:01_counter | ||
``` | ||
@@ -235,8 +264,10 @@ | ||
You can also try them in codesandbox.io: | ||
[01](https://codesandbox.io/s/github/dai-shi/use-context-selector/tree/master/examples/01_minimal) | ||
[02](https://codesandbox.io/s/github/dai-shi/use-context-selector/tree/master/examples/02_typescript) | ||
[01](https://codesandbox.io/s/github/dai-shi/use-context-selector/tree/main/examples/01_counter) | ||
[02](https://codesandbox.io/s/github/dai-shi/use-context-selector/tree/main/examples/02_person) | ||
[03](https://codesandbox.io/s/github/dai-shi/use-context-selector/tree/main/examples/03_suspense) | ||
## Related projects | ||
## Projects that use use-context-selector | ||
- [react-tracked](https://github.com/dai-shi/react-tracked) | ||
- [reactive-react-redux](https://github.com/dai-shi/reactive-react-redux) | ||
* [react-tracked](https://github.com/dai-shi/react-tracked) | ||
* [use-atom](https://github.com/dai-shi/use-atom) | ||
* [react-hooks-fetch](https://github.com/dai-shi/react-hooks-fetch) |
388
src/index.ts
@@ -1,59 +0,68 @@ | ||
/* eslint-disable @typescript-eslint/ban-ts-comment */ | ||
import { | ||
ComponentType, | ||
Context as ContextOrig, | ||
FC, | ||
MutableRefObject, | ||
Provider, | ||
createElement, | ||
createContext as createContextOrig, | ||
// @ts-ignore | ||
unstable_createMutableSource as createMutableSource, | ||
useCallback, | ||
useContext as useContextOrig, | ||
useEffect, | ||
useLayoutEffect, | ||
useMemo, | ||
// @ts-ignore | ||
unstable_useMutableSource as useMutableSource, | ||
useReducer, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
import type { | ||
ComponentType, | ||
Context as ContextOrig, | ||
MutableRefObject, | ||
Provider, | ||
ReactNode, | ||
} from 'react'; | ||
import { | ||
unstable_UserBlockingPriority as UserBlockingPriority, | ||
unstable_NormalPriority as NormalPriority, | ||
unstable_runWithPriority as runWithPriority, | ||
unstable_getCurrentPriorityLevel as getCurrentPriorityLevel, | ||
} from 'scheduler'; | ||
const isSSR = typeof window === 'undefined' | ||
|| /ServerSideRendering/.test(window.navigator && window.navigator.userAgent); | ||
const CONTEXT_VALUE = Symbol(); | ||
const ORIGINAL_PROVIDER = Symbol(); | ||
const useIsoLayoutEffect = isSSR | ||
? (fn: () => void) => fn() | ||
: useLayoutEffect; | ||
const isSSR = | ||
typeof window === 'undefined' || | ||
/ServerSideRendering/.test(window.navigator && window.navigator.userAgent); | ||
const SOURCE_SYMBOL = Symbol(); | ||
const UPDATE_SYMBOL = Symbol(); | ||
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect; | ||
const FUNCTION_SYMBOL = Symbol(); | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const functionMap = new WeakMap<Function, { [FUNCTION_SYMBOL]: Function }>(); | ||
// for preact that doesn't have runWithPriority | ||
const runWithNormalPriority = runWithPriority | ||
? (fn: () => void) => { | ||
try { | ||
runWithPriority(NormalPriority, fn); | ||
} catch (e) { | ||
if ((e as { message: unknown }).message === 'Not implemented.') { | ||
fn(); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
: (fn: () => void) => fn(); | ||
const ORIGINAL_PROVIDER = Symbol(); | ||
type Version = number; | ||
type Listener<Value> = (action: { | ||
n: Version; | ||
p?: Promise<Value>; | ||
v?: Value; | ||
}) => void; | ||
// @ts-ignore | ||
type ContextValue<Value> = { | ||
[SOURCE_SYMBOL]: any; | ||
[UPDATE_SYMBOL]: <T>(thunk: () => T) => T; | ||
[CONTEXT_VALUE]: { | ||
/* "v"alue */ v: MutableRefObject<Value>; | ||
/* versio"n" */ n: MutableRefObject<Version>; | ||
/* "l"isteners */ l: Set<Listener<Value>>; | ||
/* "u"pdate */ u: ( | ||
fn: () => void, | ||
options?: { suspense: boolean }, | ||
) => void; | ||
}; | ||
}; | ||
type RefValue<Value> = MutableRefObject<{ | ||
v: number; // "v" = version | ||
p: Value; // "p" = primary value | ||
s: Value; // "s" = secondary value | ||
l: Set<() => void>; // "l" = listeners | ||
}>; | ||
export interface Context<Value> { | ||
Provider: ComponentType<{ value: Value }>; | ||
Provider: ComponentType<{ value: Value; children: ReactNode }>; | ||
displayName?: string; | ||
@@ -63,32 +72,64 @@ } | ||
const createProvider = <Value>(ProviderOrig: Provider<ContextValue<Value>>) => { | ||
const RefProvider: FC<{ value: Value }> = ({ value, children }) => { | ||
const ref: RefValue<Value> = useRef({ | ||
v: 0, // "v" = version | ||
p: value, // "p" = primary value | ||
s: value, // "s" = secondary value | ||
l: new Set<() => void>(), // "l" = listeners | ||
}); | ||
const priorityRef = useRef(NormalPriority); | ||
const contextValue = useMemo(() => ({ | ||
[SOURCE_SYMBOL]: createMutableSource(ref, () => ref.current.v), | ||
[UPDATE_SYMBOL]: <T>(thunk: () => T) => { | ||
priorityRef.current = getCurrentPriorityLevel(); | ||
return runWithPriority(UserBlockingPriority, thunk); | ||
}, | ||
}), []); | ||
useIsoLayoutEffect(() => { | ||
if (contextValue[SOURCE_SYMBOL]._workInProgressVersionSecondary !== null) { | ||
ref.current.s = value; // update secondary value | ||
} else { | ||
ref.current.p = value; // update primary value | ||
} | ||
ref.current.v += 1; // increment version | ||
runWithPriority(priorityRef.current, () => { | ||
ref.current.l.forEach((listener) => listener()); | ||
const ContextProvider = ({ | ||
value, | ||
children, | ||
}: { | ||
value: Value; | ||
children: ReactNode; | ||
}) => { | ||
const valueRef = useRef(value); | ||
const versionRef = useRef(0); | ||
const [resolve, setResolve] = useState<((v: Value) => void) | null>(null); | ||
if (resolve) { | ||
resolve(value); | ||
setResolve(null); | ||
} | ||
const contextValue = useRef<ContextValue<Value>>(); | ||
if (!contextValue.current) { | ||
const listeners = new Set<Listener<Value>>(); | ||
const update = (fn: () => void, options?: { suspense: boolean }) => { | ||
versionRef.current += 1; | ||
const action: Parameters<Listener<Value>>[0] = { | ||
n: versionRef.current, | ||
}; | ||
if (options?.suspense) { | ||
action.n *= -1; // this is intentional to make it temporary version | ||
action.p = new Promise<Value>((r) => { | ||
setResolve(() => (v: Value) => { | ||
action.v = v; | ||
delete action.p; | ||
r(v); | ||
}); | ||
}); | ||
} | ||
listeners.forEach((listener) => listener(action)); | ||
fn(); | ||
}; | ||
contextValue.current = { | ||
[CONTEXT_VALUE]: { | ||
/* "v"alue */ v: valueRef, | ||
/* versio"n" */ n: versionRef, | ||
/* "l"isteners */ l: listeners, | ||
/* "u"pdate */ u: update, | ||
}, | ||
}; | ||
} | ||
useIsomorphicLayoutEffect(() => { | ||
valueRef.current = value; | ||
versionRef.current += 1; | ||
runWithNormalPriority(() => { | ||
(contextValue.current as ContextValue<Value>)[CONTEXT_VALUE].l.forEach( | ||
(listener) => { | ||
listener({ n: versionRef.current, v: value }); | ||
}, | ||
); | ||
}); | ||
priorityRef.current = NormalPriority; | ||
}); | ||
return createElement(ProviderOrig, { value: contextValue }, children); | ||
}, [value]); | ||
return createElement( | ||
ProviderOrig, | ||
{ value: contextValue.current }, | ||
children, | ||
); | ||
}; | ||
return RefProvider; | ||
return ContextProvider; | ||
}; | ||
@@ -98,21 +139,5 @@ | ||
const createDefaultSource = <Value>(defaultValue: Value) => { | ||
const ref: RefValue<Value> = { | ||
current: { | ||
v: -1, // "v" = version | ||
p: defaultValue, // "p" = primary value | ||
s: defaultValue, // "s" = secondary value | ||
l: new Set<() => void>(), // "l" = listeners | ||
}, | ||
}; | ||
return createMutableSource(ref, () => ref.current.v); | ||
}; | ||
/** | ||
* This creates a special context for selector-enabled `useContext`. | ||
* This creates a special context for `useContextSelector`. | ||
* | ||
* It doesn't pass its value but a ref of the value. | ||
* Unlike the original context provider, this context provider | ||
* expects the context value to be immutable and stable. | ||
* | ||
* @example | ||
@@ -125,35 +150,26 @@ * import { createContext } from 'use-context-selector'; | ||
const context = createContextOrig<ContextValue<Value>>({ | ||
[SOURCE_SYMBOL]: createDefaultSource(defaultValue), | ||
[UPDATE_SYMBOL]: (thunk) => thunk(), | ||
[CONTEXT_VALUE]: { | ||
/* "v"alue */ v: { current: defaultValue }, | ||
/* versio"n" */ n: { current: -1 }, | ||
/* "l"isteners */ l: new Set(), | ||
/* "u"pdate */ u: (f) => f(), | ||
}, | ||
}); | ||
(context as unknown as { | ||
[ORIGINAL_PROVIDER]: Provider<ContextValue<Value>>; | ||
})[ORIGINAL_PROVIDER] = context.Provider; | ||
(context as unknown as Context<Value>).Provider = createProvider(context.Provider); | ||
delete context.Consumer; // no support for Consumer | ||
( | ||
context as unknown as { | ||
[ORIGINAL_PROVIDER]: Provider<ContextValue<Value>>; | ||
} | ||
)[ORIGINAL_PROVIDER] = context.Provider; | ||
(context as unknown as Context<Value>).Provider = createProvider( | ||
context.Provider, | ||
); | ||
delete (context as { Consumer: unknown }).Consumer; // no support for Consumer | ||
return context as unknown as Context<Value>; | ||
} | ||
const subscribe = ( | ||
ref: RefValue<unknown>, | ||
callback: () => void, | ||
) => { | ||
const listeners = ref.current.l; | ||
listeners.add(callback); | ||
return () => listeners.delete(callback); | ||
}; | ||
export function useContext<Value>(context: Context<Value>): Value | ||
export function useContext<Value, Selected>( | ||
context: Context<Value>, | ||
selector: (value: Value) => Selected, | ||
): Selected | ||
/** | ||
* This hook returns context value with optional selector. | ||
* This hook returns context selected value by selector. | ||
* | ||
* It will only accept context created by `createContext`. | ||
* It will trigger re-render if only the selected value is referentially changed. | ||
* The selector must be stable. | ||
* Either define selector outside render or wrap with `useCallback`. | ||
* | ||
@@ -163,48 +179,94 @@ * The selector should return referentially equal result for same input for better performance. | ||
* @example | ||
* import { useContext } from 'use-context-selector'; | ||
* import { useContextSelector } from 'use-context-selector'; | ||
* | ||
* const firstName = useContext(PersonContext, state => state.firstName); | ||
* const firstName = useContextSelector(PersonContext, (state) => state.firstName); | ||
*/ | ||
export function useContext<Value, Selected>( | ||
export function useContextSelector<Value, Selected>( | ||
context: Context<Value>, | ||
selector: (value: Value) => Selected = identity as (value: Value) => Selected, | ||
selector: (value: Value) => Selected, | ||
) { | ||
const { [SOURCE_SYMBOL]: source } = useContextOrig( | ||
const contextValue = useContextOrig( | ||
context as unknown as ContextOrig<ContextValue<Value>>, | ||
); | ||
if (process.env.NODE_ENV !== 'production') { | ||
if (!source) { | ||
throw new Error('This useContext requires special context for selector support'); | ||
)[CONTEXT_VALUE]; | ||
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { | ||
if (!contextValue) { | ||
throw new Error('useContextSelector requires special context'); | ||
} | ||
} | ||
const getSnapshot = useCallback( | ||
(ref: RefValue<Value>) => { | ||
const value = source._workInProgressVersionSecondary !== null | ||
? ref.current.s // "s" = secondary value | ||
: ref.current.p; // "p" = primary value | ||
const selected = selector(value); | ||
if (typeof selected === 'function') { | ||
if (functionMap.has(selected)) { | ||
return functionMap.get(selected); | ||
const { | ||
/* "v"alue */ v: { current: value }, | ||
/* versio"n" */ n: { current: version }, | ||
/* "l"isteners */ l: listeners, | ||
} = contextValue; | ||
const selected = selector(value); | ||
const [state, dispatch] = useReducer( | ||
( | ||
prev: readonly [Value, Selected], | ||
action?: Parameters<Listener<Value>>[0], | ||
) => { | ||
if (!action) { | ||
// case for `dispatch()` below | ||
return [value, selected] as const; | ||
} | ||
if ('p' in action) { | ||
throw action.p; | ||
} | ||
if (action.n === version) { | ||
if (Object.is(prev[1], selected)) { | ||
return prev; // bail out | ||
} | ||
const wrappedFunction = { [FUNCTION_SYMBOL]: selected }; | ||
functionMap.set(selected, wrappedFunction); | ||
return wrappedFunction; | ||
return [value, selected] as const; | ||
} | ||
return selected; | ||
try { | ||
if ('v' in action) { | ||
if (Object.is(prev[0], action.v)) { | ||
return prev; // do not update | ||
} | ||
const nextSelected = selector(action.v); | ||
if (Object.is(prev[1], nextSelected)) { | ||
return prev; // do not update | ||
} | ||
return [action.v, nextSelected] as const; | ||
} | ||
} catch (_e) { | ||
// ignored (stale props or some other reason) | ||
} | ||
return [...prev] as const; // schedule update | ||
}, | ||
[selector, source], | ||
[value, selected] as const, | ||
); | ||
const snapshot = useMutableSource(source, getSnapshot, subscribe); | ||
if (snapshot && (snapshot as { [FUNCTION_SYMBOL]: unknown })[FUNCTION_SYMBOL]) { | ||
return snapshot[FUNCTION_SYMBOL]; | ||
if (!Object.is(state[1], selected)) { | ||
// schedule re-render | ||
// this is safe because it's self contained | ||
dispatch(); | ||
} | ||
return snapshot; | ||
useIsomorphicLayoutEffect(() => { | ||
listeners.add(dispatch); | ||
return () => { | ||
listeners.delete(dispatch); | ||
}; | ||
}, [listeners]); | ||
return state[1]; | ||
} | ||
/** | ||
* This hook returns an update function that accepts a thunk function | ||
* This hook returns the entire context value. | ||
* Use this instead of React.useContext for consistent behavior. | ||
* | ||
* Use this for a function that will change a value. | ||
* @example | ||
* import { useContext } from 'use-context-selector'; | ||
* | ||
* const person = useContext(PersonContext); | ||
*/ | ||
export function useContext<Value>(context: Context<Value>) { | ||
return useContextSelector(context, identity); | ||
} | ||
/** | ||
* This hook returns an update function to wrap an updating function | ||
* | ||
* Use this for a function that will change a value in | ||
* concurrent rendering in React 18. | ||
* Otherwise, there's no need to use this hook. | ||
* | ||
* @example | ||
@@ -214,18 +276,24 @@ * import { useContextUpdate } from 'use-context-selector'; | ||
* const update = useContextUpdate(); | ||
* | ||
* // Wrap set state function | ||
* update(() => setState(...)); | ||
* | ||
* // Experimental suspense mode | ||
* update(() => setState(...), { suspense: true }); | ||
*/ | ||
export function useContextUpdate( | ||
context: Context<unknown>, | ||
) { | ||
const { [UPDATE_SYMBOL]: update } = useContextOrig( | ||
context as unknown as ContextOrig<ContextValue<unknown>>, | ||
); | ||
if (process.env.NODE_ENV !== 'production') { | ||
if (!update) { | ||
throw new Error('This useContext requires special context for selector support'); | ||
export function useContextUpdate<Value>(context: Context<Value>) { | ||
const contextValue = useContextOrig( | ||
context as unknown as ContextOrig<ContextValue<Value>>, | ||
)[CONTEXT_VALUE]; | ||
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { | ||
if (!contextValue) { | ||
throw new Error('useContextUpdate requires special context'); | ||
} | ||
} | ||
const { u: update } = contextValue; | ||
return update; | ||
} | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/** | ||
@@ -235,3 +303,3 @@ * This is a Provider component for bridging multiple react roots | ||
* @example | ||
* const valueToBridge = useContext(PersonContext); | ||
* const valueToBridge = useBridgeValue(PersonContext); | ||
* return ( | ||
@@ -245,10 +313,15 @@ * <Renderer> | ||
*/ | ||
export const BridgeProvider: React.FC<{ | ||
export const BridgeProvider = ({ | ||
context, | ||
value, | ||
children, | ||
}: { | ||
context: Context<any>; | ||
value: any; | ||
}> = ({ context, value, children }) => { | ||
value: unknown; | ||
children: ReactNode; | ||
}) => { | ||
const { [ORIGINAL_PROVIDER]: ProviderOrig } = context as unknown as { | ||
[ORIGINAL_PROVIDER]: Provider<unknown>; | ||
}; | ||
if (process.env.NODE_ENV !== 'production') { | ||
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { | ||
if (!ProviderOrig) { | ||
@@ -260,1 +333,16 @@ throw new Error('BridgeProvider requires special context'); | ||
}; | ||
/** | ||
* This hook return a value for BridgeProvider | ||
*/ | ||
export const useBridgeValue = (context: Context<any>) => { | ||
const bridgeValue = useContextOrig( | ||
context as unknown as ContextOrig<ContextValue<unknown>>, | ||
); | ||
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { | ||
if (!bridgeValue[CONTEXT_VALUE]) { | ||
throw new Error('useBridgeValue requires special context'); | ||
} | ||
} | ||
return bridgeValue as any; | ||
}; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
26
985
2
259
Yes
43036
9
8
1