From 0bffe631f984f8a1f0a2f22406534997ff9bdfd8 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Fri, 3 Jan 2020 17:05:15 +0100 Subject: [PATCH 1/4] feat(context-selector): add initial implementation --- packages/react-context-selector/.gulp.js | 1 + packages/react-context-selector/README.md | 45 ++++++++++ .../react-context-selector/babel.config.js | 1 + packages/react-context-selector/gulpfile.ts | 1 + .../react-context-selector/jest.config.js | 5 ++ packages/react-context-selector/package.json | 40 +++++++++ .../src/createContext.ts | 80 +++++++++++++++++ packages/react-context-selector/src/index.ts | 4 + packages/react-context-selector/src/types.ts | 15 ++++ .../src/useContextSelector.ts | 61 +++++++++++++ .../src/useContextSelectors.ts | 80 +++++++++++++++++ packages/react-context-selector/src/utils.ts | 6 ++ .../test/createContext-test.tsx | 73 +++++++++++++++ .../test/useContextSelector-test.tsx | 88 +++++++++++++++++++ .../test/useContextSelectors-test.tsx | 87 ++++++++++++++++++ packages/react-context-selector/tsconfig.json | 14 +++ 16 files changed, 601 insertions(+) create mode 100644 packages/react-context-selector/.gulp.js create mode 100644 packages/react-context-selector/README.md create mode 100644 packages/react-context-selector/babel.config.js create mode 100644 packages/react-context-selector/gulpfile.ts create mode 100644 packages/react-context-selector/jest.config.js create mode 100644 packages/react-context-selector/package.json create mode 100644 packages/react-context-selector/src/createContext.ts create mode 100644 packages/react-context-selector/src/index.ts create mode 100644 packages/react-context-selector/src/types.ts create mode 100644 packages/react-context-selector/src/useContextSelector.ts create mode 100644 packages/react-context-selector/src/useContextSelectors.ts create mode 100644 packages/react-context-selector/src/utils.ts create mode 100644 packages/react-context-selector/test/createContext-test.tsx create mode 100644 packages/react-context-selector/test/useContextSelector-test.tsx create mode 100644 packages/react-context-selector/test/useContextSelectors-test.tsx create mode 100644 packages/react-context-selector/tsconfig.json diff --git a/packages/react-context-selector/.gulp.js b/packages/react-context-selector/.gulp.js new file mode 100644 index 0000000000..87c6422c6b --- /dev/null +++ b/packages/react-context-selector/.gulp.js @@ -0,0 +1 @@ +module.exports = require('../../.gulp') diff --git a/packages/react-context-selector/README.md b/packages/react-context-selector/README.md new file mode 100644 index 0000000000..79aec9be03 --- /dev/null +++ b/packages/react-context-selector/README.md @@ -0,0 +1,45 @@ +# `@fluentui/react-context-selector` + +React `useContextSelector()` and `useContextSelectors()` hooks in userland. + +## Introduction + +[React Context](https://reactjs.org/docs/context.html) and [`useContext()`](https://reactjs.org/docs/hooks-reference.html#usecontext) is often used to avoid prop drilling, +however it's known that there's a performance issue. When a context value is changed, all components that are subscribed with `useContext()` will re-render. + +[useContextSelector](https://github.com/reactjs/rfcs/pull/119) is recently proposed. While waiting for the process, this library provides the API in userland. + +# Installation + +**NPM** + +```bash +npm install --save @fluentui/react-context-selector +``` + +**Yarn** + +```bash +yarn add @fluentui/react-context-selector +``` + +## Usage + +TODO + +## Technical memo + +React context by nature triggers propagation of component re-rendering if a value is changed. To avoid this, this library uses undocumented feature of `calculateChangedBits`. It then uses a subscription model to force update when a component needs to re-render. + +## 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`. +- `` components are not supported. +- The [stale props](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) issue can't be solved in userland. (workaround with try-catch) + +## Related projects + +The implementation is heavily inspired by: + +- [use-context-selector](https://github.com/dai-shi/use-context-selector) +- [react-tracked](https://github.com/dai-shi/react-tracked) diff --git a/packages/react-context-selector/babel.config.js b/packages/react-context-selector/babel.config.js new file mode 100644 index 0000000000..a1c212ad45 --- /dev/null +++ b/packages/react-context-selector/babel.config.js @@ -0,0 +1 @@ +module.exports = api => require('@fluentui/internal-tooling/babel')(api) diff --git a/packages/react-context-selector/gulpfile.ts b/packages/react-context-selector/gulpfile.ts new file mode 100644 index 0000000000..de10829664 --- /dev/null +++ b/packages/react-context-selector/gulpfile.ts @@ -0,0 +1 @@ +import '../../gulpfile' diff --git a/packages/react-context-selector/jest.config.js b/packages/react-context-selector/jest.config.js new file mode 100644 index 0000000000..8c78ce7fdf --- /dev/null +++ b/packages/react-context-selector/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + ...require('@fluentui/internal-tooling/jest'), + name: 'react-context-selector', + moduleNameMapper: require('lerna-alias').jest(), +} diff --git a/packages/react-context-selector/package.json b/packages/react-context-selector/package.json new file mode 100644 index 0000000000..52eb931315 --- /dev/null +++ b/packages/react-context-selector/package.json @@ -0,0 +1,40 @@ +{ + "name": "@fluentui/react-context-selector", + "description": "React useContextSelector & useContextSelectors hooks in userland", + "version": "0.43.0", + "author": "Oleksandr Fediashov ", + "bugs": "https://github.com/microsoft/fluent-ui-react/issues", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "devDependencies": { + "@fluentui/internal-tooling": "^0.43.0", + "@types/react-is": "^16.7.1", + "lerna-alias": "^3.0.3-0", + "react": "^16.8.0", + "react-is": "^16.6.3" + }, + "peerDependencies": { + "react": "^16.8.0" + }, + "files": [ + "dist" + ], + "homepage": "https://github.com/microsoft/fluent-ui-react/tree/master/packages/react-context-selector", + "jsnext:main": "dist/es/index.js", + "license": "MIT", + "main": "dist/commonjs/index.js", + "module": "dist/es/index.js", + "publishConfig": { + "access": "public" + }, + "repository": "microsoft/fluent-ui-react.git", + "scripts": { + "build": "gulp bundle:package:no-umd", + "clean": "gulp bundle:package:clean", + "test": "gulp test", + "test:watch": "gulp test:watch" + }, + "sideEffects": false, + "types": "dist/es/index.d.ts" +} diff --git a/packages/react-context-selector/src/createContext.ts b/packages/react-context-selector/src/createContext.ts new file mode 100644 index 0000000000..d031984cfc --- /dev/null +++ b/packages/react-context-selector/src/createContext.ts @@ -0,0 +1,80 @@ +import * as React from 'react' +import { Context, ContextListener, ContextValue } from './types' + +// Stops React Context propagation +// https://github.com/facebook/react/blob/95bd7aad7daa80c381faa3215c80b0906ab5ead5/packages/react-reconciler/src/ReactFiberBeginWork.js#L2656 +const calculateChangedBits = () => 0 + +const createProvider = (Original: React.Provider>) => { + const Provider: React.FC> = props => { + const listeners = React.useRef[]>([]) + + // We call listeners in render intentionally. Listeners are not technically pure, but + // otherwise we can't get benefits from concurrent mode. + // + // We make sure to work with double or more invocation of listeners. + listeners.current.forEach(listener => listener(props.value)) + + // Disables updates propogation for React Context as `value` is always shallow equal + const subscribe = React.useCallback((listener: ContextListener) => { + listeners.current.push(listener) + + const unsubscribe = () => { + const index = listeners.current.indexOf(listener) + listeners.current.splice(index, 1) + } + + return unsubscribe + }, []) + + return React.createElement( + Original, + { value: { subscribe, value: props.value } }, + props.children, + ) + } + + /* istanbul ignore else */ + if (process.env.NODE_ENV !== 'production') { + Provider.displayName = 'ContextSelector.Provider' + } + + return Provider +} + +type CreateContextOptions = { + strict?: boolean +} + +export const createContext = ( + defaultValue: Value, + options: CreateContextOptions = {}, +): Context => { + const { strict = true } = options + + const context = React.createContext>( + { + get subscribe() { + if (strict) { + /* istanbul ignore next */ + throw new Error( + process.env.NODE_ENV === 'production' + ? '' + : `Please use component from "@fluentui/react-context-selector"`, + ) + } + + /* istanbul ignore next */ + return () => () => {} + }, + value: defaultValue, + }, + calculateChangedBits, + ) + context.Provider = createProvider(context.Provider) as any + + // We don't support Consumer API + delete context.Consumer + + return context as any +} diff --git a/packages/react-context-selector/src/index.ts b/packages/react-context-selector/src/index.ts new file mode 100644 index 0000000000..85d7a77ebf --- /dev/null +++ b/packages/react-context-selector/src/index.ts @@ -0,0 +1,4 @@ +export { createContext } from './createContext' +export { useContextSelector } from './useContextSelector' +export { useContextSelectors } from './useContextSelectors' +export * from './types' diff --git a/packages/react-context-selector/src/types.ts b/packages/react-context-selector/src/types.ts new file mode 100644 index 0000000000..e8906ce8f8 --- /dev/null +++ b/packages/react-context-selector/src/types.ts @@ -0,0 +1,15 @@ +import * as React from 'react' + +export type Context = React.Context & { + Provider: React.FC> + Consumer: never +} + +export type ContextListener = (value: Value) => void + +export type ContextSelector = (value: Value) => SelectedValue + +export type ContextValue = { + subscribe: (listener: ContextListener) => any + value: Value +} diff --git a/packages/react-context-selector/src/useContextSelector.ts b/packages/react-context-selector/src/useContextSelector.ts new file mode 100644 index 0000000000..d90dd3cbed --- /dev/null +++ b/packages/react-context-selector/src/useContextSelector.ts @@ -0,0 +1,61 @@ +import * as React from 'react' + +import { Context, ContextSelector, ContextValue } from './types' +import { useIsomorphicLayoutEffect } from './utils' + +type UseSelectorRef = { + selector: ContextSelector + selected: SelectedValue + value: Value +} + +/** + * 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 referencially changed. + */ +export const useContextSelector = ( + context: Context, + selector: ContextSelector, +): SelectedValue => { + const { subscribe, value } = React.useContext( + (context as unknown) as Context>, + ) + const [, forceUpdate] = React.useReducer((c: number) => c + 1, 0) as [never, () => void] + + const ref = React.useRef>() + const selected = selector(value) + + useIsomorphicLayoutEffect(() => { + ref.current = { + selector, + value, + selected, + } + }) + useIsomorphicLayoutEffect(() => { + const callback = (nextState: Value) => { + try { + const reference: UseSelectorRef = ref.current as NonNullable< + UseSelectorRef + > + + if ( + reference.value === nextState || + Object.is(reference.selected, reference.selector(nextState)) + ) { + // not changed + return + } + } catch (e) { + // ignored (stale props or some other reason) + } + + forceUpdate() + } + + return subscribe(callback) + }, [subscribe]) + + return selected +} diff --git a/packages/react-context-selector/src/useContextSelectors.ts b/packages/react-context-selector/src/useContextSelectors.ts new file mode 100644 index 0000000000..bc9afafb00 --- /dev/null +++ b/packages/react-context-selector/src/useContextSelectors.ts @@ -0,0 +1,80 @@ +import * as React from 'react' + +import { Context, ContextSelector, ContextValue } from './types' +import { useIsomorphicLayoutEffect } from './utils' + +type UseSelectorRef< + Value, + Properties extends string, + Selectors extends Record>, + SelectedValue extends any +> = { + selectors: Selectors + value: Value + selected: Record +} + +/** + * This hook returns context selected value by selectors. + * It will only accept context created by `createContext`. + * It will trigger re-render if only the selected value is referencially changed. + */ +export const useContextSelectors = < + Value, + Properties extends string, + Selectors extends Record>, + SelectedValue extends any +>( + context: Context, + selectors: Selectors, +): Record => { + const { subscribe, value } = React.useContext( + (context as unknown) as Context>, + ) + const [, forceUpdate] = React.useReducer((c: number) => c + 1, 0) as [never, () => void] + + const ref = React.useRef>() + const selected = {} as Record + + Object.keys(selectors).forEach((key: Properties) => { + selected[key] = selectors[key](value) + }) + + useIsomorphicLayoutEffect(() => { + ref.current = { + selectors, + value, + selected, + } + }) + useIsomorphicLayoutEffect(() => { + const callback = (nextState: Value) => { + try { + const reference: UseSelectorRef< + Value, + Properties, + Selectors, + SelectedValue + > = ref.current as NonNullable> + + if ( + reference.value === nextState || + Object.keys(reference.selected).every((key: Properties) => + Object.is(reference.selected[key], reference.selectors[key](nextState)), + ) + ) { + // not changed + return + } + } catch (e) { + // ignored (stale props or some other reason) + } + + forceUpdate() + } + + return subscribe(callback) + }, [subscribe]) + + return selected +} diff --git a/packages/react-context-selector/src/utils.ts b/packages/react-context-selector/src/utils.ts new file mode 100644 index 0000000000..7afd4eabda --- /dev/null +++ b/packages/react-context-selector/src/utils.ts @@ -0,0 +1,6 @@ +import * as React from 'react' + +// useLayoutEffect that does not show warning when server-side rendering, see Alex Reardon's article for more info +// @see https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? React.useLayoutEffect : /* istanbul ignore next */ React.useEffect diff --git a/packages/react-context-selector/test/createContext-test.tsx b/packages/react-context-selector/test/createContext-test.tsx new file mode 100644 index 0000000000..da488cf169 --- /dev/null +++ b/packages/react-context-selector/test/createContext-test.tsx @@ -0,0 +1,73 @@ +import { createContext, useContextSelector } from '@fluentui/react-context-selector' +import { mount } from 'enzyme' +import * as React from 'react' +import * as ReactIs from 'react-is' + +class TestBoundary extends React.Component<{ onError: (e: Error) => void }, { hasError: boolean }> { + state = { hasError: false } + + componentDidCatch(error: Error) { + this.props.onError(error) + this.setState({ hasError: true }) + } + + render() { + if (this.state.hasError) { + return null + } + + return this.props.children + } +} + +describe('createContext', () => { + it('creates a Provider component', () => { + const Context = createContext(null) + expect(ReactIs.isValidElementType(Context.Provider)).toBeTruthy() + }) + + describe('options', () => { + it('throws on usage outside Provider by default', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + + const TestContext = createContext('') + const TestComponent: React.FC = () => { + const value = useContextSelector(TestContext, v => v) + return
+ } + + const onError = jest.fn() + mount( + + + , + ) + + expect(onError).toBeCalledWith( + expect.objectContaining({ + message: 'Please use component from "@fluentui/react-context-selector"', + }), + ) + + // We need to clean up mocks to avoid errors reported by React + ;(console.error as any).mockClear() + }) + + it('do not throw usage outside Provider when `strict` is `false`', () => { + const TestContext = createContext('', { strict: false }) + const TestComponent: React.FC = () => { + const value = useContextSelector(TestContext, v => v) + return
+ } + + const onError = jest.fn() + mount( + + + , + ) + + expect(onError).not.toBeCalled() + }) + }) +}) diff --git a/packages/react-context-selector/test/useContextSelector-test.tsx b/packages/react-context-selector/test/useContextSelector-test.tsx new file mode 100644 index 0000000000..4f303fc9a4 --- /dev/null +++ b/packages/react-context-selector/test/useContextSelector-test.tsx @@ -0,0 +1,88 @@ +import { createContext, useContextSelector } from '@fluentui/react-context-selector' +import { mount } from 'enzyme' +import * as React from 'react' + +const TestContext = createContext<{ index: number }>({ index: -1 }) + +const TestComponent: React.FC<{ index: number; onUpdate?: () => void }> = props => { + const active = useContextSelector(TestContext, v => v.index === props.index) + + React.useEffect(() => { + props.onUpdate && props.onUpdate() + }) + + return
+} + +describe('useContextSelector', () => { + it('propogates values via Context', () => { + const wrapper = mount( + + + , + ) + + expect(wrapper.find('div').prop('data-active')).toBe(true) + }) + + it('updates only on selector match', () => { + const onUpdate = jest.fn() + const wrapper = mount( + + + , + ) + + expect(wrapper.find('div').prop('data-active')).toBe(false) + expect(onUpdate).toBeCalledTimes(1) + + // No match, (v.index: 2, p.index: 1) + wrapper.setProps({ value: { index: 2 } }) + expect(wrapper.find('div').prop('data-active')).toBe(false) + expect(onUpdate).toBeCalledTimes(1) + + // Match => update, (v.index: 1, p.index: 1) + wrapper.setProps({ value: { index: 1 } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(onUpdate).toBeCalledTimes(2) + + // Match previous => no update, (v.index: 1, p.index: 1) + wrapper.setProps({ value: { index: 1 } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(onUpdate).toBeCalledTimes(2) + }) + + it('updates are propogated inside React.memo()', () => { + // https://reactjs.org/docs/react-api.html#reactmemo + // Will never pass updates + const MemoComponent = React.memo(TestComponent, () => true) + + const onUpdate = jest.fn() + const wrapper = mount( + + + , + ) + + wrapper.setProps({ value: { index: 1 } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(onUpdate).toBeCalledTimes(2) + }) + + it('handles unsubscribe', () => { + const MemoComponent = React.memo(TestComponent) + const onUpdate = jest.fn() + + const wrapper = mount( + + + + , + ) + + wrapper.setProps({ + children: [null, ], + }) + expect(onUpdate).toBeCalledTimes(1) + }) +}) diff --git a/packages/react-context-selector/test/useContextSelectors-test.tsx b/packages/react-context-selector/test/useContextSelectors-test.tsx new file mode 100644 index 0000000000..0cfd991d59 --- /dev/null +++ b/packages/react-context-selector/test/useContextSelectors-test.tsx @@ -0,0 +1,87 @@ +import { createContext, useContextSelectors } from '@fluentui/react-context-selector' +import { mount } from 'enzyme' +import * as React from 'react' + +const TestContext = createContext<{ index: number; value: string }>({ + index: -1, + value: '', +}) + +const TestComponent: React.FC<{ index: number; onUpdate?: () => void }> = props => { + const context = useContextSelectors(TestContext, { + active: v => v.index === props.index, + value: v => v.value, + }) + + React.useEffect(() => { + props.onUpdate && props.onUpdate() + }) + + return
+} + +describe('useContextSelectors', () => { + it('propogates values via Context', () => { + const wrapper = mount( + + + , + ) + + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(wrapper.find('div').prop('data-value')).toBe('foo') + }) + + it('updates only on selector match', () => { + const onUpdate = jest.fn() + const wrapper = mount( + + + , + ) + + expect(wrapper.find('div').prop('data-active')).toBe(false) + expect(wrapper.find('div').prop('data-value')).toBe('foo') + expect(onUpdate).toBeCalledTimes(1) + + // No match, (v.index: 2, p.index: 1) + wrapper.setProps({ value: { index: 2, value: 'foo' } }) + expect(wrapper.find('div').prop('data-active')).toBe(false) + expect(wrapper.find('div').prop('data-value')).toBe('foo') + expect(onUpdate).toBeCalledTimes(1) + + // Match => update, (v.index: 1, p.index: 1) + wrapper.setProps({ value: { index: 1, value: 'foo' } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(wrapper.find('div').prop('data-value')).toBe('foo') + expect(onUpdate).toBeCalledTimes(2) + + // Match previous => no update, (v.index: 1, p.index: 1) + wrapper.setProps({ value: { index: 1, value: 'foo' } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(wrapper.find('div').prop('data-value')).toBe('foo') + expect(onUpdate).toBeCalledTimes(2) + + // Match => update, (v.value: 'bar') + wrapper.setProps({ value: { index: 1, value: 'bar' } }) + expect(wrapper.find('div').prop('data-value')).toBe('bar') + expect(onUpdate).toBeCalledTimes(3) + }) + + it('updates are propogated inside React.memo()', () => { + // https://reactjs.org/docs/react-api.html#reactmemo + // Will never pass updates + const MemoComponent = React.memo(TestComponent, () => true) + + const onUpdate = jest.fn() + const wrapper = mount( + + + , + ) + + wrapper.setProps({ value: { index: 1, value: 'foo' } }) + expect(wrapper.find('div').prop('data-active')).toBe(true) + expect(onUpdate).toBeCalledTimes(2) + }) +}) diff --git a/packages/react-context-selector/tsconfig.json b/packages/react-context-selector/tsconfig.json new file mode 100644 index 0000000000..6908043d43 --- /dev/null +++ b/packages/react-context-selector/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../build/tsconfig.common", + "compilerOptions": { + "composite": true, + "outDir": "dist/dts", + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "noUnusedParameters": true, + "strictNullChecks": true + }, + "include": ["src", "test"], + "references": [] +} From 01bffe5e614b2b95070968bfb6bdb6b19d8a2c80 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 12 Feb 2020 12:36:12 +0100 Subject: [PATCH 2/4] fix review comments --- packages/react-context-selector/package.json | 2 +- packages/react-context-selector/src/createContext.ts | 12 +++++------- packages/react-context-selector/src/index.ts | 6 +++--- packages/react-context-selector/src/types.ts | 4 ++++ .../react-context-selector/src/useContextSelector.ts | 4 +++- .../src/useContextSelectors.ts | 12 +++++++----- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/react-context-selector/package.json b/packages/react-context-selector/package.json index 52eb931315..4e4e8c0357 100644 --- a/packages/react-context-selector/package.json +++ b/packages/react-context-selector/package.json @@ -8,7 +8,7 @@ "@babel/runtime": "^7.1.2" }, "devDependencies": { - "@fluentui/internal-tooling": "^0.43.0", + "@fluentui/internal-tooling": "^0.44.0", "@types/react-is": "^16.7.1", "lerna-alias": "^3.0.3-0", "react": "^16.8.0", diff --git a/packages/react-context-selector/src/createContext.ts b/packages/react-context-selector/src/createContext.ts index d031984cfc..4c1cd28296 100644 --- a/packages/react-context-selector/src/createContext.ts +++ b/packages/react-context-selector/src/createContext.ts @@ -1,5 +1,5 @@ import * as React from 'react' -import { Context, ContextListener, ContextValue } from './types' +import { Context, ContextListener, ContextValue, CreateContextOptions } from './types' // Stops React Context propagation // https://github.com/facebook/react/blob/95bd7aad7daa80c381faa3215c80b0906ab5ead5/packages/react-reconciler/src/ReactFiberBeginWork.js#L2656 @@ -42,11 +42,7 @@ const createProvider = (Original: React.Provider>) => return Provider } -type CreateContextOptions = { - strict?: boolean -} - -export const createContext = ( +const createContext = ( defaultValue: Value, options: CreateContextOptions = {}, ): Context => { @@ -76,5 +72,7 @@ export const createContext = ( // We don't support Consumer API delete context.Consumer - return context as any + return (context as unknown) as Context } + +export default createContext diff --git a/packages/react-context-selector/src/index.ts b/packages/react-context-selector/src/index.ts index 85d7a77ebf..9f8f0f3613 100644 --- a/packages/react-context-selector/src/index.ts +++ b/packages/react-context-selector/src/index.ts @@ -1,4 +1,4 @@ -export { createContext } from './createContext' -export { useContextSelector } from './useContextSelector' -export { useContextSelectors } from './useContextSelectors' +export { default as createContext } from './createContext' +export { default as useContextSelector } from './useContextSelector' +export { default as useContextSelectors } from './useContextSelectors' export * from './types' diff --git a/packages/react-context-selector/src/types.ts b/packages/react-context-selector/src/types.ts index e8906ce8f8..1847125baa 100644 --- a/packages/react-context-selector/src/types.ts +++ b/packages/react-context-selector/src/types.ts @@ -5,6 +5,10 @@ export type Context = React.Context & { Consumer: never } +export type CreateContextOptions = { + strict?: boolean +} + export type ContextListener = (value: Value) => void export type ContextSelector = (value: Value) => SelectedValue diff --git a/packages/react-context-selector/src/useContextSelector.ts b/packages/react-context-selector/src/useContextSelector.ts index d90dd3cbed..b2a651cdab 100644 --- a/packages/react-context-selector/src/useContextSelector.ts +++ b/packages/react-context-selector/src/useContextSelector.ts @@ -14,7 +14,7 @@ type UseSelectorRef = { * It will only accept context created by `createContext`. * It will trigger re-render if only the selected value is referencially changed. */ -export const useContextSelector = ( +const useContextSelector = ( context: Context, selector: ContextSelector, ): SelectedValue => { @@ -59,3 +59,5 @@ export const useContextSelector = ( return selected } + +export default useContextSelector diff --git a/packages/react-context-selector/src/useContextSelectors.ts b/packages/react-context-selector/src/useContextSelectors.ts index bc9afafb00..0224726f63 100644 --- a/packages/react-context-selector/src/useContextSelectors.ts +++ b/packages/react-context-selector/src/useContextSelectors.ts @@ -3,7 +3,7 @@ import * as React from 'react' import { Context, ContextSelector, ContextValue } from './types' import { useIsomorphicLayoutEffect } from './utils' -type UseSelectorRef< +type UseSelectorsRef< Value, Properties extends string, Selectors extends Record>, @@ -19,7 +19,7 @@ type UseSelectorRef< * It will only accept context created by `createContext`. * It will trigger re-render if only the selected value is referencially changed. */ -export const useContextSelectors = < +const useContextSelectors = < Value, Properties extends string, Selectors extends Record>, @@ -33,7 +33,7 @@ export const useContextSelectors = < ) const [, forceUpdate] = React.useReducer((c: number) => c + 1, 0) as [never, () => void] - const ref = React.useRef>() + const ref = React.useRef>() const selected = {} as Record Object.keys(selectors).forEach((key: Properties) => { @@ -50,12 +50,12 @@ export const useContextSelectors = < useIsomorphicLayoutEffect(() => { const callback = (nextState: Value) => { try { - const reference: UseSelectorRef< + const reference: UseSelectorsRef< Value, Properties, Selectors, SelectedValue - > = ref.current as NonNullable> + > = ref.current as NonNullable> if ( reference.value === nextState || @@ -78,3 +78,5 @@ export const useContextSelectors = < return selected } + +export default useContextSelectors From e8be1385245a2aab8081d19d0815e94257f68a7a Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 12 Feb 2020 12:38:01 +0100 Subject: [PATCH 3/4] remove todo --- packages/react-context-selector/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-context-selector/README.md b/packages/react-context-selector/README.md index 79aec9be03..b7c8eac4fc 100644 --- a/packages/react-context-selector/README.md +++ b/packages/react-context-selector/README.md @@ -23,10 +23,6 @@ npm install --save @fluentui/react-context-selector yarn add @fluentui/react-context-selector ``` -## Usage - -TODO - ## Technical memo React context by nature triggers propagation of component re-rendering if a value is changed. To avoid this, this library uses undocumented feature of `calculateChangedBits`. It then uses a subscription model to force update when a component needs to re-render. From ac1e0a5f03d60430b660ec818861f10e696fe752 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 12 Feb 2020 12:46:06 +0100 Subject: [PATCH 4/4] bump version to align with other packages --- packages/react-context-selector/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-context-selector/package.json b/packages/react-context-selector/package.json index 4e4e8c0357..871d2044f8 100644 --- a/packages/react-context-selector/package.json +++ b/packages/react-context-selector/package.json @@ -5,7 +5,7 @@ "author": "Oleksandr Fediashov ", "bugs": "https://github.com/microsoft/fluent-ui-react/issues", "dependencies": { - "@babel/runtime": "^7.1.2" + "@babel/runtime": "^7.7.6" }, "devDependencies": { "@fluentui/internal-tooling": "^0.44.0",