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..b7c8eac4fc
--- /dev/null
+++ b/packages/react-context-selector/README.md
@@ -0,0 +1,41 @@
+# `@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
+```
+
+## 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..871d2044f8
--- /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.7.6"
+ },
+ "devDependencies": {
+ "@fluentui/internal-tooling": "^0.44.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..4c1cd28296
--- /dev/null
+++ b/packages/react-context-selector/src/createContext.ts
@@ -0,0 +1,78 @@
+import * as React from 'react'
+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
+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
+}
+
+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 unknown) as Context
+}
+
+export default createContext
diff --git a/packages/react-context-selector/src/index.ts b/packages/react-context-selector/src/index.ts
new file mode 100644
index 0000000000..9f8f0f3613
--- /dev/null
+++ b/packages/react-context-selector/src/index.ts
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000000..1847125baa
--- /dev/null
+++ b/packages/react-context-selector/src/types.ts
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+export type Context = React.Context & {
+ Provider: React.FC>
+ Consumer: never
+}
+
+export type CreateContextOptions = {
+ strict?: boolean
+}
+
+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..b2a651cdab
--- /dev/null
+++ b/packages/react-context-selector/src/useContextSelector.ts
@@ -0,0 +1,63 @@
+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.
+ */
+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
+}
+
+export default useContextSelector
diff --git a/packages/react-context-selector/src/useContextSelectors.ts b/packages/react-context-selector/src/useContextSelectors.ts
new file mode 100644
index 0000000000..0224726f63
--- /dev/null
+++ b/packages/react-context-selector/src/useContextSelectors.ts
@@ -0,0 +1,82 @@
+import * as React from 'react'
+
+import { Context, ContextSelector, ContextValue } from './types'
+import { useIsomorphicLayoutEffect } from './utils'
+
+type UseSelectorsRef<
+ 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.
+ */
+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: UseSelectorsRef<
+ 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
+}
+
+export default useContextSelectors
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": []
+}