Skip to content

[v2] new API and new implementation with useMutableSource #12

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

Closed
wants to merge 90 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
2ba7353
wip: v2
dai-shi Feb 14, 2020
507fab6
yarn, webpack config
dai-shi Feb 14, 2020
fbc92a1
wip: v2 with useMutableSource
dai-shi Feb 14, 2020
2784e96
wip: some fixes
dai-shi Feb 14, 2020
a07830a
wip: memo context value
dai-shi Feb 14, 2020
2322910
wip: combine useMemo
dai-shi Feb 14, 2020
51f6a5e
use npm-run-all
dai-shi Feb 14, 2020
c10cf75
update dev dependencies
dai-shi Feb 14, 2020
de0117f
update README
dai-shi Feb 14, 2020
41416fb
wip: simplify a bit more
dai-shi Feb 15, 2020
eaa5b3d
update comment
dai-shi Feb 15, 2020
bdd17af
ts-jest config
dai-shi Feb 15, 2020
b88b623
follow new API
dai-shi Feb 15, 2020
26c8a16
named importing hooks
dai-shi Feb 16, 2020
62b4dcb
update code with upcoming useMutableSource
dai-shi Feb 20, 2020
656ab8c
new API: selector in the 2nd arg of useContext
dai-shi Feb 20, 2020
67a6c32
update with the latest RFC
dai-shi Feb 20, 2020
c924b2a
revert handleChange
dai-shi Feb 20, 2020
ff52b56
update docs
dai-shi Feb 21, 2020
2450505
use layout effect with priority
dai-shi Feb 22, 2020
34c9281
move mutation in layout effect, but this would lead tearing.
dai-shi Feb 22, 2020
6e1869b
failing tearing spec
dai-shi Feb 22, 2020
0bc13d2
end up using calculateChangedBits, while that was the major motivatio…
dai-shi Feb 22, 2020
1e26281
merge v1
dai-shi Feb 24, 2020
e96fac6
merge v1 again
dai-shi Feb 24, 2020
883e14e
convert basic spec, why failing?
dai-shi Feb 24, 2020
68f5698
revert calculateChangedBits hack
dai-shi Feb 24, 2020
b783367
examples follow new api
dai-shi Feb 24, 2020
a0d921a
we should probably give up with solving tearing with parent.
dai-shi Feb 28, 2020
e12e4fb
runtime check message
dai-shi Feb 28, 2020
b318953
clean up a left over, update README
dai-shi Feb 28, 2020
76fd2d6
Merge branch 'v1' into v2
dai-shi Mar 2, 2020
6a6f401
merge master
dai-shi Mar 2, 2020
537e1d9
update README
dai-shi Mar 2, 2020
db9f7b2
update README
dai-shi Mar 4, 2020
6d248f1
a workaround for the possible bug in useMutableSource
dai-shi Mar 6, 2020
8b35dec
remove scheduler, update dev deps
dai-shi Mar 8, 2020
8a8f228
jest --preset
dai-shi Mar 8, 2020
1dec8e3
update dependencies
dai-shi Mar 12, 2020
aed3d1d
update README
dai-shi Mar 12, 2020
aee640c
add stale props spec (failing)
dai-shi Mar 12, 2020
cbadd4d
run compile
dai-shi Mar 12, 2020
448f8be
update package.json
dai-shi Mar 17, 2020
13eed63
update yarn.lock
dai-shi Mar 22, 2020
09bb83c
Merge branch 'master' into v2
dai-shi Mar 22, 2020
76ae3da
Merge branch 'master' into v2
dai-shi Mar 22, 2020
b641936
Merge branch 'master' into v2
dai-shi Mar 22, 2020
b70e336
use scheduler to control priority for useTransition
dai-shi Mar 24, 2020
8e64f1b
merge master
dai-shi Mar 24, 2020
5ed8c3a
ended up using any
dai-shi Mar 24, 2020
f05a7c9
run compile; 2.0.0-alpha.2
dai-shi Mar 24, 2020
4adef15
apidoc
dai-shi Mar 24, 2020
0069b8c
fix typo
dai-shi Mar 24, 2020
7464250
export custom Context type
dai-shi Mar 24, 2020
4da2478
2.0.0-alpha.3
dai-shi Mar 24, 2020
96d4197
fix format
dai-shi Mar 24, 2020
ef50db3
update dependencies
dai-shi Apr 10, 2020
8d0698d
update README
dai-shi Apr 11, 2020
0a78b8c
merge master
dai-shi May 22, 2020
43507b6
fix eslint rule
dai-shi May 22, 2020
2fcf1a2
wrap function for getSnapshot workaround
dai-shi May 22, 2020
b4d59ea
2.0.0-alpha.4
dai-shi May 31, 2020
2dff03d
run compile
dai-shi May 31, 2020
f187fb4
update README
dai-shi May 31, 2020
3b72120
merge master
dai-shi Jul 2, 2020
b571171
v2.0.0-alpha.5
dai-shi Jul 2, 2020
7f952c8
update dev deps and unstable_ prefix
dai-shi Aug 3, 2020
ce4ae34
v2.0.0-alpha.6
dai-shi Aug 3, 2020
1120fb2
fix typo
dai-shi Aug 18, 2020
ba4b50f
proper default context value
dai-shi Sep 2, 2020
076b6a9
trying to support branching
dai-shi Sep 2, 2020
163e226
change the priority utility function to a hook
dai-shi Sep 17, 2020
feba3aa
v2.0.0-alpha.7
dai-shi Sep 17, 2020
faa7e75
merge master
dai-shi Sep 17, 2020
5acfa3f
merge master
dai-shi Sep 24, 2020
deb2170
useIsoLayoutEffect from master
dai-shi Sep 24, 2020
7b52117
implement BridgeProvider
dai-shi Sep 24, 2020
389582d
user blocking priority for update
dai-shi Sep 24, 2020
4446313
v2.0.0-alpha.9
dai-shi Sep 29, 2020
c7dc332
merge master
dai-shi Oct 1, 2020
c1edd25
fix test
dai-shi Oct 1, 2020
f1bfa64
merge master
dai-shi Oct 1, 2020
c3b41b0
merge master
dai-shi Oct 2, 2020
2ec77f4
merge master
dai-shi Oct 3, 2020
9b3d3b2
merge master and update deps
dai-shi Nov 9, 2020
fb28545
Merge branch 'master' into v2
dai-shi Nov 28, 2020
08908dc
merge master and adjust everything
dai-shi Dec 1, 2020
24016c5
merge master
dai-shi Dec 3, 2020
5aea090
merge master; keep useBridgeValue
dai-shi Dec 12, 2020
bb8a2e5
fix: test/06
dai-shi Dec 12, 2020
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
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"import/extensions": ["error", "never"],
"import/no-unresolved": ["error", { "ignore": ["use-context-selector"] }],
"symbol-description": "off",
"no-underscore-dangle": "off",
"no-use-before-define": "off",
"no-redeclare": "off",
"no-unused-vars": "off"
},
"overrides": [{
Expand Down
53 changes: 29 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![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)

React useContextSelector hook in userland
React useContext with selector support in userland

## Introduction

Expand All @@ -22,6 +22,7 @@ This library provides the API in userland.

Prior to v1.3, it uses `changedBits=0` feature to stop propagation,
v1.3 no longer depends on this undocumented feature.
v2 uses `useMutableSource`.

## Install

Expand Down Expand Up @@ -97,13 +98,33 @@ const App = () => (
ReactDOM.render(<App />, document.getElementById('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

<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

### createContext

This creates a special context for `useContextSelector`.
This creates a special context for selector-enabled `useContext`.

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

Expand All @@ -117,43 +138,28 @@ import { createContext } from 'use-context-selector';
const PersonContext = createContext({ firstName: '', familyName: '' });
```

### useContextSelector
### useContext

This hook returns context selected value by selector.
This hook returns context value with optional 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`.

The selector should return referentially equal result for same input for better performance.

#### Parameters

- `context` **Context&lt;Value>**
- `selector` **function (value: Value): Selected**

#### 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&lt;Value>**
- `selector` **function (value: Value): Selected** (optional, default `identity as(value:Value)=>Selected`)

#### Examples

```javascript
import { useContext } from 'use-context-selector';

const person = useContext(PersonContext);
const firstName = useContext(PersonContext, state => state.firstName);
```

### useContextUpdate
Expand Down Expand Up @@ -212,7 +218,6 @@ This hook return a value for BridgeProvider
## 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`.
- 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)
Expand Down
9 changes: 5 additions & 4 deletions __tests__/01_basic_spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
Dispatch,
SetStateAction,
useCallback,
useRef,
useState,
StrictMode,
Expand All @@ -12,7 +13,7 @@ import {

import {
createContext,
useContextSelector,
useContext,
} from '../src/index';

describe('basic spec', () => {
Expand All @@ -31,8 +32,8 @@ describe('basic spec', () => {
[initialState, () => null],
);
const Counter1 = () => {
const count1 = useContextSelector(context, (v) => v[0].count1);
const setState = useContextSelector(context, (v) => v[1]);
const count1 = useContext(context, useCallback((v) => v[0].count1, []));
const setState = useContext(context, useCallback((v) => v[1], []));
const increment = () => setState((s) => ({
...s,
count1: s.count1 + 1,
Expand All @@ -48,7 +49,7 @@ describe('basic spec', () => {
);
};
const Counter2 = () => {
const count2 = useContextSelector(context, (v) => v[0].count2);
const count2 = useContext(context, useCallback((v) => v[0].count2, []));
const renderCount = useRef(0);
renderCount.current += 1;
return (
Expand Down
8 changes: 4 additions & 4 deletions __tests__/02_tearing_spec.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState, StrictMode } from 'react';
import React, { useCallback, useState, StrictMode } from 'react';

import { render, fireEvent, cleanup } from '@testing-library/react';

import { createContext, useContextSelector } from '../src/index';
import { createContext, useContext } from '../src/index';

describe.skip('tearing spec', () => {
afterEach(cleanup);
Expand All @@ -13,7 +13,7 @@ describe.skip('tearing spec', () => {
};
const context = createContext(initialState);
const Counter: React.FC<{ parentCount: number }> = ({ parentCount }) => {
const count = useContextSelector(context, (v) => v.count);
const count = useContext(context, useCallback((v) => v.count, []));
if (parentCount !== count) throw new Error('tears!!!');
return (
<div>
Expand All @@ -22,7 +22,7 @@ describe.skip('tearing spec', () => {
</div>
);
};
const Parent = () => {
const Parent: React.FC = () => {
const [state, setState] = useState(initialState);
const increment = () => setState((s) => ({
...s,
Expand Down
16 changes: 8 additions & 8 deletions __tests__/03_stale_props_spec.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import React, { useReducer } from 'react';
import React, { useCallback, useReducer } from 'react';

import { render, fireEvent, cleanup } from '@testing-library/react';

import { createContext, useContextSelector } from '../src/index';
import { createContext, useContext } from '../src/index';

describe.skip('stale props spec', () => {
afterEach(cleanup);

it('ignores transient errors in selector (e.g. due to stale props)', () => {
const Context = createContext(0);
const Parent: React.FC = () => {
const count = useContextSelector(Context, (c: number) => c);
const count = useContext(Context, useCallback((c: number) => c, []));
return <Child parentCount={count} />;
};
const Child: React.FC<{ parentCount: number }> = ({ parentCount }) => {
const result = useContextSelector(Context, (c: number) => {
const result = useContext(Context, useCallback((c: number) => {
if (c !== parentCount) {
throw new Error();
}
return c + parentCount;
});
}, [parentCount]));
return <div>{result}</div>;
};
const App: React.FC = () => {
Expand All @@ -43,14 +43,14 @@ describe.skip('stale props spec', () => {
let selectorSawInconsistencies = false;
const Context = createContext(0);
const Parent: React.FC = () => {
const count = useContextSelector(Context, (c: number) => c);
const count = useContext(Context, useCallback((c: number) => c, []));
return <Child parentCount={count} />;
};
const Child: React.FC<{ parentCount: number }> = ({ parentCount }) => {
const result = useContextSelector(Context, (c: number) => {
const result = useContext(Context, useCallback((c: number) => {
selectorSawInconsistencies = selectorSawInconsistencies || c !== parentCount;
return c + parentCount;
});
}, [parentCount]));
return <div>{result}</div>;
};
const App: React.FC = () => {
Expand Down
9 changes: 5 additions & 4 deletions __tests__/05_cm_spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
Dispatch,
SetStateAction,
useCallback,
useRef,
useState,
StrictMode,
Expand All @@ -12,7 +13,7 @@ import {

import {
createContext,
useContextSelector,
useContext,
useContextUpdate,
} from '../src/index';

Expand All @@ -32,8 +33,8 @@ describe('useContextUpdate spec', () => {
[initialState, () => null],
);
const Counter1 = () => {
const count1 = useContextSelector(context, (v) => v[0].count1);
const setState = useContextSelector(context, (v) => v[1]);
const count1 = useContext(context, useCallback((v) => v[0].count1, []));
const setState = useContext(context, useCallback((v) => v[1], []));
const update = useContextUpdate(context);
const increment = () => update(() => {
setState((s) => ({
Expand All @@ -52,7 +53,7 @@ describe('useContextUpdate spec', () => {
);
};
const Counter2 = () => {
const count2 = useContextSelector(context, (v) => v[0].count2);
const count2 = useContext(context, useCallback((v) => v[0].count2, []));
const renderCount = useRef(0);
renderCount.current += 1;
return (
Expand Down
9 changes: 5 additions & 4 deletions __tests__/06_bridge_spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
Dispatch,
SetStateAction,
useCallback,
useState,
StrictMode,
} from 'react';
Expand All @@ -11,7 +12,7 @@ import {

import {
createContext,
useContextSelector,
useContext,
useBridgeValue,
BridgeProvider,
} from '../src/index';
Expand All @@ -31,8 +32,8 @@ describe('Bridge spec', () => {
[initialState, () => null],
);
const Counter = () => {
const count = useContextSelector(context, (v) => v[0].count);
const setState = useContextSelector(context, (v) => v[1]);
const count = useContext(context, useCallback((v) => v[0].count, []));
const setState = useContext(context, useCallback((v) => v[1], []));
const increment = () => {
setState((s) => ({
...s,
Expand All @@ -47,7 +48,7 @@ describe('Bridge spec', () => {
);
};
const AnotherCounter = () => {
const count = useContextSelector(context, (v) => v[0].count);
const count = useContext(context, useCallback((v) => v[0].count, []));
return (
<div>
<span data-testid="anothercounter">{count}</span>
Expand Down
2 changes: 1 addition & 1 deletion __tests__/__snapshots__/05_cm_spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ exports[`useContextUpdate spec counter 2`] = `
+1
</button>
<span>
5
3
</span>
</div>
<div>
Expand Down
12 changes: 6 additions & 6 deletions examples/01_minimal/src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useReducer, StrictMode } from 'react';
import React, { useCallback, useReducer, StrictMode } from 'react';
import ReactDOM from 'react-dom';

import {
createContext,
useContextSelector,
useContext,
} from 'use-context-selector';

const initialState = {
Expand All @@ -23,8 +23,8 @@ const reducer = (state, action) => {
const context = createContext(null);

const Counter = () => {
const count = useContextSelector(context, (v) => v[0].count);
const dispatch = useContextSelector(context, (v) => v[1]);
const count = useContext(context, useCallback((v) => v[0].count, []));
const dispatch = useContext(context, useCallback((v) => v[1], []));
return (
<div>
{Math.random()}
Expand All @@ -38,8 +38,8 @@ const Counter = () => {
};

const TextBox = () => {
const text = useContextSelector(context, (v) => v[0].text);
const dispatch = useContextSelector(context, (v) => v[1]);
const text = useContext(context, useCallback((v) => v[0].text, []));
const dispatch = useContext(context, useCallback((v) => v[1], []));
return (
<div>
{Math.random()}
Expand Down
8 changes: 4 additions & 4 deletions examples/02_typescript/src/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import React, { useCallback } from 'react';

import { useContextSelector } from 'use-context-selector';
import { useContext } from 'use-context-selector';

import { MyContext } from './state';

const Counter = () => {
const count = useContextSelector(MyContext, (v) => v[0].count);
const dispatch = useContextSelector(MyContext, (v) => v[1]);
const count = useContext(MyContext, useCallback((v) => v[0].count, []));
const dispatch = useContext(MyContext, useCallback((v) => v[1], []));
return (
<div>
{Math.random()}
Expand Down
8 changes: 4 additions & 4 deletions examples/02_typescript/src/Person.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import React, { useCallback } from 'react';

import { useContextSelector } from 'use-context-selector';
import { useContext } from 'use-context-selector';

import { MyContext } from './state';

const Person = () => {
const person = useContextSelector(MyContext, (v) => v[0].person);
const dispatch = useContextSelector(MyContext, (v) => v[1]);
const person = useContext(MyContext, useCallback((v) => v[0].person, []));
const dispatch = useContext(MyContext, useCallback((v) => v[1], []));
return (
<div>
{Math.random()}
Expand Down
Loading