From 42e75e0c956a1d2afb40f0cb0147c31156ea3ae0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 9 Apr 2020 17:52:22 +0000 Subject: [PATCH 01/17] Add new hooks used in migrating Fabric components to functional components --- packages/react-hooks/etc/react-hooks.api.md | 10 ++- packages/react-hooks/src/index.ts | 2 + .../src/useControllableValue.test.tsx | 76 +++++++++++++++++++ .../react-hooks/src/useControllableValue.ts | 24 ++++++ packages/react-hooks/src/useId.ts | 6 +- .../react-hooks/src/useMergedRefs.test.tsx | 64 ++++++++++++++++ packages/react-hooks/src/useMergedRefs.ts | 19 +++++ 7 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 packages/react-hooks/src/useControllableValue.test.tsx create mode 100644 packages/react-hooks/src/useControllableValue.ts create mode 100644 packages/react-hooks/src/useMergedRefs.test.tsx create mode 100644 packages/react-hooks/src/useMergedRefs.ts diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 98d49a91c8f492..c8265fc4a00091 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -4,6 +4,8 @@ ```ts +import * as React from 'react'; + // @public export interface IUseBooleanCallbacks { setFalse: () => void; @@ -21,7 +23,13 @@ export function useConst(initialValue: T | (() => T)): T; export function useConstCallback any>(callback: T): T; // @public -export function useId(prefix?: string): string; +export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): readonly [TValue | undefined, React.Dispatch>]; + +// @public +export function useId(prefix?: string, providedId?: string): string; + +// @public +export function useMergedRefs(...refs: React.Ref[]): (value: T) => void; // (No @packageDocumentation comment for this package) diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 53d722b24ed64d..afdc14eee27cb9 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -3,3 +3,5 @@ export * from './useBoolean'; export * from './useConst'; export * from './useConstCallback'; export * from './useId'; +export * from './useMergedRefs'; +export * from './useControllableValue'; diff --git a/packages/react-hooks/src/useControllableValue.test.tsx b/packages/react-hooks/src/useControllableValue.test.tsx new file mode 100644 index 00000000000000..44f50c7399d5eb --- /dev/null +++ b/packages/react-hooks/src/useControllableValue.test.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import { useControllableValue } from './useControllableValue'; + +describe('useControllableValue', () => { + it('respects controlled value', () => { + let resultValue: boolean | undefined; + const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ + value, + defaultValue, + }) => { + [resultValue] = useControllableValue(value, defaultValue); + return
; + }; + + const wrapper1 = mount(); + expect(resultValue!).toBe(true); + + wrapper1.setProps({ value: false }); + expect(resultValue!).toBe(false); + + const wrapper2 = mount(); + expect(resultValue!).toBe(false); + + wrapper2.setProps({ value: true }); + expect(resultValue!).toBe(true); + }); + + it('uses the default value if no controlled value is provided', () => { + let resultValue: boolean | undefined; + const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ + value, + defaultValue, + }) => { + [resultValue] = useControllableValue(value, defaultValue); + return
; + }; + + mount(); + expect(resultValue!).toBe(true); + }); + + it('uses an updated controlled value over a default value', () => { + let resultValue: boolean | undefined; + const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ + value, + defaultValue, + }) => { + [resultValue] = useControllableValue(value, defaultValue); + return
; + }; + + const wrapper = mount(); + expect(resultValue!).toBe(true); + + wrapper.setProps({ value: false, defaultValue: true }); + expect(resultValue!).toBe(false); + }); + + it('does not change value when the default value changes', () => { + let resultValue: boolean | undefined; + const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ + value, + defaultValue, + }) => { + [resultValue] = useControllableValue(value, defaultValue); + return
; + }; + + const wrapper = mount(); + expect(resultValue!).toBe(true); + + wrapper.setProps({ defaultValue: false }); + expect(resultValue!).toBe(true); + }); +}); diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts new file mode 100644 index 00000000000000..b7c94b6f2f5794 --- /dev/null +++ b/packages/react-hooks/src/useControllableValue.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; + +/** + * Hook to manage a value that could be either controlled or uncontrolled, such as a checked state or + * text box string. + * @param controlledValue- The controlled value passed in the props. This value will always be used if provided, and the + * internal state will be updated to reflect it. + * @param defaultUncontrolledValue- Initial value for the internal state in the uncontrolled case. + * @see https://reactjs.org/docs/uncontrolled-components.html + */ +export function useControllableValue( + controlledValue: TValue | undefined, + defaultUncontrolledValue: TValue | undefined, +) { + const [value, setValue] = React.useState( + controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, + ); + + if (controlledValue !== undefined && controlledValue !== value) { + setValue(controlledValue); + } + + return [value, setValue] as const; +} diff --git a/packages/react-hooks/src/useId.ts b/packages/react-hooks/src/useId.ts index 8ae535643c1cf0..9ee94a79491c8b 100644 --- a/packages/react-hooks/src/useId.ts +++ b/packages/react-hooks/src/useId.ts @@ -5,12 +5,14 @@ import { getId } from '@uifabric/utilities/lib/getId'; * Hook to generate a unique ID in the global scope (spanning across duplicate copies of the same library). * * @param prefix - Optional prefix for the ID + * @param providedId - Optional id provided by a parent component. Defaults to the provided value if present, + * without conditioning the hook call * @returns The ID */ -export function useId(prefix?: string): string { +export function useId(prefix?: string, providedId?: string): string { // getId should only be called once since it updates the global constant for the next ID value. // (While an extra update isn't likely to cause problems in practice, it's better to avoid it.) - const ref = React.useRef(); + const ref = React.useRef(providedId); if (!ref.current) { ref.current = getId(prefix); } diff --git a/packages/react-hooks/src/useMergedRefs.test.tsx b/packages/react-hooks/src/useMergedRefs.test.tsx new file mode 100644 index 00000000000000..af4f7f8db2bbde --- /dev/null +++ b/packages/react-hooks/src/useMergedRefs.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import { useMergedRefs } from './useMergedRefs'; + +describe('useMergedRefs', () => { + it('updates all provided refs', () => { + const refObject: React.RefObject = React.createRef(); + let refValue: boolean | null = null; + const TestComponent: React.FunctionComponent = () => { + const mergedRef = useMergedRefs(refObject, val => (refValue = val)); + mergedRef(true); + return null; + }; + mount(); + + expect(refObject.current).toBe(true); + expect(refValue).toBe(true); + }); + + it('reuses the same ref callback if refs remain stable', () => { + const refObject: React.RefObject = React.createRef(); + + const refValueFunc = (val: boolean) => {}; + + let refCallback: Function | undefined = undefined; + const TestComponent: React.FunctionComponent = () => { + refCallback = useMergedRefs(refObject, refValueFunc); + return null; + }; + + const wrapper = mount(); + + const firstRefCallback = refCallback; + + // Re-render the component + wrapper.update(); + + expect(refCallback).toBe(firstRefCallback); + }); + + it('handles changing ref callbacks', () => { + const refObject: React.RefObject = React.createRef(); + + let firstRefValue: boolean | null = null; + let refValueFunc = (val: boolean) => (firstRefValue = val); + + const TestComponent: React.FunctionComponent = () => { + const mergedRef = useMergedRefs(refObject, refValueFunc); + mergedRef(true); + return null; + }; + + const wrapper = mount(); + + let secondRefValue: boolean | null = null; + refValueFunc = (val: boolean) => (secondRefValue = val); + + // Re-render the component + wrapper.setProps({}); + + expect(firstRefValue).toBe(true); + expect(secondRefValue).toBe(true); + }); +}); diff --git a/packages/react-hooks/src/useMergedRefs.ts b/packages/react-hooks/src/useMergedRefs.ts new file mode 100644 index 00000000000000..5d51bc43804cfd --- /dev/null +++ b/packages/react-hooks/src/useMergedRefs.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +/** + * React hook to merge multile React refs (either MutableRefObjects or ref callbacks) into a single ref callback that + * updates all provided refs + * @param refs- Refs to collectively update with one ref value. + */ +export function useMergedRefs(...refs: React.Ref[]) { + return React.useCallback((value: T) => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value); + } else if (ref) { + // work around the immutability of the React.Ref type + ((ref as unknown) as React.MutableRefObject).current = value; + } + }); + }, refs); +} From ee05549aa4f706e9dbe8395c2ce005eb4db48d11 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 9 Apr 2020 17:52:46 +0000 Subject: [PATCH 02/17] Change files --- ...react-hooks-2020-04-09-17-52-46-functional-hooks.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 change/@uifabric-react-hooks-2020-04-09-17-52-46-functional-hooks.json diff --git a/change/@uifabric-react-hooks-2020-04-09-17-52-46-functional-hooks.json b/change/@uifabric-react-hooks-2020-04-09-17-52-46-functional-hooks.json new file mode 100644 index 00000000000000..e6ef227d539054 --- /dev/null +++ b/change/@uifabric-react-hooks-2020-04-09-17-52-46-functional-hooks.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "comment": "Add new hooks used in migrating Fabric components to functional components", + "packageName": "@uifabric/react-hooks", + "email": "miclo@microsoft.com", + "commit": "42e75e0c956a1d2afb40f0cb0147c31156ea3ae0", + "dependentChangeType": "patch", + "date": "2020-04-09T17:52:46.940Z" +} \ No newline at end of file From 9733ebd98d3ebf3785beac3424d26211e27270f8 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 9 Apr 2020 18:34:32 +0000 Subject: [PATCH 03/17] Cherry pick revision from MLoughry:functional/checkbox --- packages/react-hooks/etc/react-hooks.api.md | 2 +- .../react-hooks/src/useControllableValue.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index c8265fc4a00091..0b6cc555308d00 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -23,7 +23,7 @@ export function useConst(initialValue: T | (() => T)): T; export function useConstCallback any>(callback: T): T; // @public -export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): readonly [TValue | undefined, React.Dispatch>]; +export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: (newValue: TValue, ...args: TOnChangeArgs) => void): readonly [TValue | undefined, (newValue: TValue, ...args: TOnChangeArgs) => void]; // @public export function useId(prefix?: string, providedId?: string): string; diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index b7c94b6f2f5794..68f33c92541a80 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -8,9 +8,10 @@ import * as React from 'react'; * @param defaultUncontrolledValue- Initial value for the internal state in the uncontrolled case. * @see https://reactjs.org/docs/uncontrolled-components.html */ -export function useControllableValue( +export function useControllableValue( controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, + onChange?: (newValue: TValue, ...args: TOnChangeArgs) => void, ) { const [value, setValue] = React.useState( controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, @@ -20,5 +21,17 @@ export function useControllableValue( setValue(controlledValue); } - return [value, setValue] as const; + const setValueOrCallOnChange = React.useCallback( + (newValue: TValue, ...args: TOnChangeArgs) => { + if (onChange) { + onChange(newValue, ...args); + } + if (controlledValue === undefined) { + setValue(newValue); + } + }, + [onChange, controlledValue], + ); + + return [value, setValueOrCallOnChange] as const; } From abb37ef88c3e71ee19013d2d51bd19a03c744d2f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 9 Apr 2020 19:08:59 +0000 Subject: [PATCH 04/17] Fix lint error --- packages/react-hooks/src/useMergedRefs.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-hooks/src/useMergedRefs.test.tsx b/packages/react-hooks/src/useMergedRefs.test.tsx index af4f7f8db2bbde..d0f63d0f8dfcd0 100644 --- a/packages/react-hooks/src/useMergedRefs.test.tsx +++ b/packages/react-hooks/src/useMergedRefs.test.tsx @@ -20,6 +20,7 @@ describe('useMergedRefs', () => { it('reuses the same ref callback if refs remain stable', () => { const refObject: React.RefObject = React.createRef(); + // tslint:disable-next-line:no-empty const refValueFunc = (val: boolean) => {}; let refCallback: Function | undefined = undefined; From 92bd1a1c5ce36b3969e7ad35ab51a080a8b39ace Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2020 17:53:02 +0000 Subject: [PATCH 05/17] Address comments from dzearing --- packages/react-hooks/src/useControllableValue.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 68f33c92541a80..03b85f7092d7e3 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -17,10 +17,6 @@ export function useControllableValue( controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, ); - if (controlledValue !== undefined && controlledValue !== value) { - setValue(controlledValue); - } - const setValueOrCallOnChange = React.useCallback( (newValue: TValue, ...args: TOnChangeArgs) => { if (onChange) { @@ -30,8 +26,8 @@ export function useControllableValue( setValue(newValue); } }, - [onChange, controlledValue], + [onChange, controlledValue === undefined], ); - return [value, setValueOrCallOnChange] as const; + return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange] as const; } From 9d941395d915515d26b75a2e77bba06e6d734a2d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2020 17:53:13 +0000 Subject: [PATCH 06/17] Fix launch.json for debugging tests --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 313b079040eddf..e1acee5270f195 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,7 +27,7 @@ "stopOnEntry": false, "args": ["-i", "--testPathPattern=\\b${fileBasenameNoExtension}", "--watch"], "runtimeExecutable": null, - "runtimeArgs": ["--nolazy", "--debug"], + "runtimeArgs": ["--nolazy", "--inspect"], "env": { "NODE_ENV": "development" }, From 3e0c28253395cc615f1b038ef8c01d9f7cf81d15 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2020 22:12:09 +0000 Subject: [PATCH 07/17] Use common onChange call signature --- packages/react-hooks/etc/react-hooks.api.md | 5 ++- .../react-hooks/src/useControllableValue.ts | 32 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 0b6cc555308d00..0727aa77b95d3e 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -22,8 +22,11 @@ export function useConst(initialValue: T | (() => T)): T; // @public export function useConstCallback any>(callback: T): T; +// Warning: (ae-forgotten-export) The symbol "ChangeCallback" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Setter" needs to be exported by the entry point index.d.ts +// // @public -export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: (newValue: TValue, ...args: TOnChangeArgs) => void): readonly [TValue | undefined, (newValue: TValue, ...args: TOnChangeArgs) => void]; +export function useControllableValue | undefined, TSetter extends Setter>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: TCallback): [TValue | undefined, TSetter]; // @public export function useId(prefix?: string, providedId?: string): string; diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 03b85f7092d7e3..2f9521179f8378 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -1,5 +1,18 @@ import * as React from 'react'; +type ChangeCallback = ( + ev: React.FormEvent, + newValue: TValue | undefined, +) => void; + +type Setter< + TValue, + TElement extends HTMLElement, + TCallback extends ChangeCallback | undefined +> = TCallback extends ChangeCallback + ? (newValue: TValue | undefined, ev: React.FormEvent) => void + : (newValue: TValue | undefined) => void; + /** * Hook to manage a value that could be either controlled or uncontrolled, such as a checked state or * text box string. @@ -8,26 +21,31 @@ import * as React from 'react'; * @param defaultUncontrolledValue- Initial value for the internal state in the uncontrolled case. * @see https://reactjs.org/docs/uncontrolled-components.html */ -export function useControllableValue( +export function useControllableValue< + TValue, + TElement extends HTMLElement, + TCallback extends ChangeCallback | undefined, + TSetter extends Setter +>( controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, - onChange?: (newValue: TValue, ...args: TOnChangeArgs) => void, -) { + onChange?: TCallback, +): [TValue | undefined, TSetter] { const [value, setValue] = React.useState( controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, ); const setValueOrCallOnChange = React.useCallback( - (newValue: TValue, ...args: TOnChangeArgs) => { + (newValue: TValue | undefined, ev?: React.FormEvent) => { if (onChange) { - onChange(newValue, ...args); + onChange(ev!, newValue); } if (controlledValue === undefined) { setValue(newValue); } }, [onChange, controlledValue === undefined], - ); + ) as TSetter; - return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange] as const; + return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange]; } From 8f8bb37bb65f78b57245518a0e950011cc3ebb9c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2020 22:25:35 +0000 Subject: [PATCH 08/17] Fix useControllableValue types in case with no onChange handler --- packages/react-hooks/etc/react-hooks.api.md | 8 ++++-- .../react-hooks/src/useControllableValue.ts | 28 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 0727aa77b95d3e..8b6db935a3e5ba 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -22,11 +22,13 @@ export function useConst(initialValue: T | (() => T)): T; // @public export function useConstCallback any>(callback: T): T; +// @public +export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>; + // Warning: (ae-forgotten-export) The symbol "ChangeCallback" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "Setter" needs to be exported by the entry point index.d.ts // -// @public -export function useControllableValue | undefined, TSetter extends Setter>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: TCallback): [TValue | undefined, TSetter]; +// @public (undocumented) +export function useControllableValue | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>; // @public export function useId(prefix?: string, providedId?: string): string; diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 2f9521179f8378..00cb6ed1ba49a4 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -5,14 +5,6 @@ type ChangeCallback = ( newValue: TValue | undefined, ) => void; -type Setter< - TValue, - TElement extends HTMLElement, - TCallback extends ChangeCallback | undefined -> = TCallback extends ChangeCallback - ? (newValue: TValue | undefined, ev: React.FormEvent) => void - : (newValue: TValue | undefined) => void; - /** * Hook to manage a value that could be either controlled or uncontrolled, such as a checked state or * text box string. @@ -21,16 +13,24 @@ type Setter< * @param defaultUncontrolledValue- Initial value for the internal state in the uncontrolled case. * @see https://reactjs.org/docs/uncontrolled-components.html */ +export function useControllableValue( + controlledValue: TValue | undefined, + defaultUncontrolledValue: TValue | undefined, +): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>; export function useControllableValue< TValue, TElement extends HTMLElement, - TCallback extends ChangeCallback | undefined, - TSetter extends Setter + TCallback extends ChangeCallback | undefined >( controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, - onChange?: TCallback, -): [TValue | undefined, TSetter] { + onChange: TCallback, +): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>; +export function useControllableValue< + TValue, + TElement extends HTMLElement, + TCallback extends ChangeCallback | undefined +>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: TCallback) { const [value, setValue] = React.useState( controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, ); @@ -45,7 +45,7 @@ export function useControllableValue< } }, [onChange, controlledValue === undefined], - ) as TSetter; + ); - return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange]; + return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange] as const; } From cf76ce0fc7617b3c63c2aebad981eea142123e37 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 17:18:34 +0000 Subject: [PATCH 09/17] Export ChangeCallback type --- packages/react-hooks/etc/react-hooks.api.md | 5 +++-- packages/react-hooks/src/useControllableValue.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 8b6db935a3e5ba..9b09f253bd0c0d 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -6,6 +6,9 @@ import * as React from 'react'; +// @public (undocumented) +export type ChangeCallback = (ev: React.FormEvent, newValue: TValue | undefined) => void; + // @public export interface IUseBooleanCallbacks { setFalse: () => void; @@ -25,8 +28,6 @@ export function useConstCallback any>(callback: T) // @public export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>; -// Warning: (ae-forgotten-export) The symbol "ChangeCallback" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export function useControllableValue | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>; diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 00cb6ed1ba49a4..72c36709b8c4df 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -type ChangeCallback = ( +export type ChangeCallback = ( ev: React.FormEvent, newValue: TValue | undefined, ) => void; From 68c226b2ec836fad5478415c8b9b61ba99b63558 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 17:25:41 +0000 Subject: [PATCH 10/17] Persist the controlled vs uncontrolled state --- packages/react-hooks/src/useControllableValue.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 72c36709b8c4df..f08f9e5a99eb6f 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useConst } from './useConst'; export type ChangeCallback = ( ev: React.FormEvent, @@ -34,18 +35,19 @@ export function useControllableValue< const [value, setValue] = React.useState( controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, ); + const isControlled = useConst(!!controlledValue); const setValueOrCallOnChange = React.useCallback( (newValue: TValue | undefined, ev?: React.FormEvent) => { if (onChange) { onChange(ev!, newValue); } - if (controlledValue === undefined) { + if (!isControlled) { setValue(newValue); } }, - [onChange, controlledValue === undefined], + [onChange], ); - return [controlledValue !== undefined ? controlledValue : value, setValueOrCallOnChange] as const; + return [isControlled ? controlledValue : value, setValueOrCallOnChange] as const; } From a1679ca3ddc89ed8241869c1bafe8d811dfd1332 Mon Sep 17 00:00:00 2001 From: Michael Loughry Date: Thu, 16 Apr 2020 10:26:09 -0700 Subject: [PATCH 11/17] Update packages/react-hooks/src/useMergedRefs.ts Co-Authored-By: Elizabeth Craig --- packages/react-hooks/src/useMergedRefs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/src/useMergedRefs.ts b/packages/react-hooks/src/useMergedRefs.ts index 5d51bc43804cfd..2d58813990a885 100644 --- a/packages/react-hooks/src/useMergedRefs.ts +++ b/packages/react-hooks/src/useMergedRefs.ts @@ -1,7 +1,7 @@ import * as React from 'react'; /** - * React hook to merge multile React refs (either MutableRefObjects or ref callbacks) into a single ref callback that + * React hook to merge multiple React refs (either MutableRefObjects or ref callbacks) into a single ref callback that * updates all provided refs * @param refs- Refs to collectively update with one ref value. */ From a940f0556981b04a29c3a515c33137bda64a7a66 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 17:39:44 +0000 Subject: [PATCH 12/17] Update README --- packages/react-hooks/README.md | 31 +++++++++++++++++++++ packages/react-hooks/etc/react-hooks.api.md | 2 +- packages/react-hooks/src/useMergedRefs.ts | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/react-hooks/README.md b/packages/react-hooks/README.md index cdb2b1e1f12688..fa46e0cf20b689 100644 --- a/packages/react-hooks/README.md +++ b/packages/react-hooks/README.md @@ -109,3 +109,34 @@ const MyComponent = () => { // ... code that shows a dialog when a button is clicked ... }; ``` + +## useControllableValue + +`function useControllableValue( controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, ): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>` + +`function useControllableValue< TValue, TElement extends HTMLElement, TCallback extends ChangeCallback | undefined \>( controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback, ): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>` + +Hook to manage the current value for a component that could be either controlled or uncontrolled, such as a checkbox or input field. + +It's two required parameters are the `controlledValue` (the current calue of the control in the controlled state), and the `defaultUncontrolledValue` (for the uncontrolled state). Optionally, you may pass a third `onChange` callback to be notified of any changes triggered by the control. + +The return value will be a setter function that will set the internal state in the unctrolled state, and invoke the onChange callback if present. + +See [React docs](https://reactjs.org/docs/uncontrolled-components.html) about the distinction between controlled and uncontrolled components. + +## useMergedRefs + +`function useMergedRefs(...refs: React.Ref[]): (instance: T) => void` + +Hook to merge multiple refs (such as one passed in as a prop and one used locally) into a single ref callback that can be passed on to a child component. + +```typescriptreact +const Example = React.forwardRef(function Example(props:{}, forwardedRef: React.Ref) { + const localRef = React.useRef(); + const mergedRef = useMergedRef(localRef, forwardedRef); + + React.useEffect(() => { localRef.current.focus() }, []); + + return
Example
; +}) +``` diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 9b09f253bd0c0d..033125ff835f6a 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -35,7 +35,7 @@ export function useControllableValue(...refs: React.Ref[]): (value: T) => void; +export function useMergedRefs(...refs: React.Ref[]): (instance: T) => void; // (No @packageDocumentation comment for this package) diff --git a/packages/react-hooks/src/useMergedRefs.ts b/packages/react-hooks/src/useMergedRefs.ts index 5d51bc43804cfd..e1816091e5078b 100644 --- a/packages/react-hooks/src/useMergedRefs.ts +++ b/packages/react-hooks/src/useMergedRefs.ts @@ -5,7 +5,7 @@ import * as React from 'react'; * updates all provided refs * @param refs- Refs to collectively update with one ref value. */ -export function useMergedRefs(...refs: React.Ref[]) { +export function useMergedRefs(...refs: React.Ref[]): (instance: T) => void { return React.useCallback((value: T) => { refs.forEach(ref => { if (typeof ref === 'function') { From 8284656b278d9f6dd893c5dc6c6ffe15c5f3c807 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 17:45:04 +0000 Subject: [PATCH 13/17] Allow for undefined events in the onChange callback --- packages/react-hooks/etc/react-hooks.api.md | 4 ++-- packages/react-hooks/src/useControllableValue.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/react-hooks/etc/react-hooks.api.md b/packages/react-hooks/etc/react-hooks.api.md index 033125ff835f6a..bc8c0f303e0db2 100644 --- a/packages/react-hooks/etc/react-hooks.api.md +++ b/packages/react-hooks/etc/react-hooks.api.md @@ -7,7 +7,7 @@ import * as React from 'react'; // @public (undocumented) -export type ChangeCallback = (ev: React.FormEvent, newValue: TValue | undefined) => void; +export type ChangeCallback = (ev: React.FormEvent | undefined, newValue: TValue | undefined) => void; // @public export interface IUseBooleanCallbacks { @@ -29,7 +29,7 @@ export function useConstCallback any>(callback: T) export function useControllableValue(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>; // @public (undocumented) -export function useControllableValue | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>; +export function useControllableValue | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev?: React.FormEvent) => void]>; // @public export function useId(prefix?: string, providedId?: string): string; diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index f08f9e5a99eb6f..9b6e82828f7069 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { useConst } from './useConst'; export type ChangeCallback = ( - ev: React.FormEvent, + ev: React.FormEvent | undefined, newValue: TValue | undefined, ) => void; @@ -26,15 +26,13 @@ export function useControllableValue< controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback, -): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev: React.FormEvent) => void]>; +): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev?: React.FormEvent) => void]>; export function useControllableValue< TValue, TElement extends HTMLElement, TCallback extends ChangeCallback | undefined >(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: TCallback) { - const [value, setValue] = React.useState( - controlledValue !== undefined ? controlledValue : defaultUncontrolledValue, - ); + const [value, setValue] = React.useState(defaultUncontrolledValue); const isControlled = useConst(!!controlledValue); const setValueOrCallOnChange = React.useCallback( From a8c5d07b0e3d3f7b72fd78914abac5b640497e10 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 18:03:15 +0000 Subject: [PATCH 14/17] Fix error in isControlled value --- packages/react-hooks/src/useControllableValue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/src/useControllableValue.ts b/packages/react-hooks/src/useControllableValue.ts index 9b6e82828f7069..92d9b4e7800653 100644 --- a/packages/react-hooks/src/useControllableValue.ts +++ b/packages/react-hooks/src/useControllableValue.ts @@ -33,7 +33,7 @@ export function useControllableValue< TCallback extends ChangeCallback | undefined >(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange?: TCallback) { const [value, setValue] = React.useState(defaultUncontrolledValue); - const isControlled = useConst(!!controlledValue); + const isControlled = useConst(controlledValue !== undefined); const setValueOrCallOnChange = React.useCallback( (newValue: TValue | undefined, ev?: React.FormEvent) => { From d6e3f6b2cde9e376d2e900daa4986343da6764f0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 16 Apr 2020 18:03:34 +0000 Subject: [PATCH 15/17] Delete test for deprecated behavior --- .../src/useControllableValue.test.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/react-hooks/src/useControllableValue.test.tsx b/packages/react-hooks/src/useControllableValue.test.tsx index 44f50c7399d5eb..e7d64a84d55d0b 100644 --- a/packages/react-hooks/src/useControllableValue.test.tsx +++ b/packages/react-hooks/src/useControllableValue.test.tsx @@ -40,23 +40,6 @@ describe('useControllableValue', () => { expect(resultValue!).toBe(true); }); - it('uses an updated controlled value over a default value', () => { - let resultValue: boolean | undefined; - const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ - value, - defaultValue, - }) => { - [resultValue] = useControllableValue(value, defaultValue); - return
; - }; - - const wrapper = mount(); - expect(resultValue!).toBe(true); - - wrapper.setProps({ value: false, defaultValue: true }); - expect(resultValue!).toBe(false); - }); - it('does not change value when the default value changes', () => { let resultValue: boolean | undefined; const TestComponent: React.FunctionComponent<{ value?: boolean; defaultValue?: boolean }> = ({ From 4fbb3e2613f0c9a27d18294d747f7ca49d579675 Mon Sep 17 00:00:00 2001 From: Michael Loughry Date: Thu, 16 Apr 2020 11:04:10 -0700 Subject: [PATCH 16/17] Update packages/react-hooks/README.md Co-Authored-By: Elizabeth Craig --- packages/react-hooks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/README.md b/packages/react-hooks/README.md index fa46e0cf20b689..e739466f328d40 100644 --- a/packages/react-hooks/README.md +++ b/packages/react-hooks/README.md @@ -118,7 +118,7 @@ const MyComponent = () => { Hook to manage the current value for a component that could be either controlled or uncontrolled, such as a checkbox or input field. -It's two required parameters are the `controlledValue` (the current calue of the control in the controlled state), and the `defaultUncontrolledValue` (for the uncontrolled state). Optionally, you may pass a third `onChange` callback to be notified of any changes triggered by the control. +Its two required parameters are the `controlledValue` (the current value of the control in the controlled state), and the `defaultUncontrolledValue` (for the uncontrolled state). Optionally, you may pass a third `onChange` callback to be notified of any changes triggered by the control. The return value will be a setter function that will set the internal state in the unctrolled state, and invoke the onChange callback if present. From a7d0e420a3efc044dec91a039b68137f5f307cc3 Mon Sep 17 00:00:00 2001 From: Michael Loughry Date: Thu, 16 Apr 2020 11:05:06 -0700 Subject: [PATCH 17/17] Update packages/react-hooks/README.md Co-Authored-By: Elizabeth Craig --- packages/react-hooks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/README.md b/packages/react-hooks/README.md index e739466f328d40..0a410cf8f22723 100644 --- a/packages/react-hooks/README.md +++ b/packages/react-hooks/README.md @@ -120,7 +120,7 @@ Hook to manage the current value for a component that could be either controlled Its two required parameters are the `controlledValue` (the current value of the control in the controlled state), and the `defaultUncontrolledValue` (for the uncontrolled state). Optionally, you may pass a third `onChange` callback to be notified of any changes triggered by the control. -The return value will be a setter function that will set the internal state in the unctrolled state, and invoke the onChange callback if present. +The return value will be a setter function that will set the internal state in the uncontrolled state, and invoke the `onChange` callback if present. See [React docs](https://reactjs.org/docs/uncontrolled-components.html) about the distinction between controlled and uncontrolled components.