-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add useControlledValue * Add unit tests * Remove usage of renderHook * Allow null as a value
- Loading branch information
1 parent
b3c43db
commit 30a4c46
Showing
3 changed files
with
136 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { default as useControlledState } from './use-controlled-state'; | ||
export { default as useJumpStep } from './use-jump-step'; | ||
export { default as useUpdateEffect } from './use-update-effect'; | ||
export { useControlledValue } from './use-controlled-value'; |
101 changes: 101 additions & 0 deletions
101
packages/components/src/utils/hooks/test/use-controlled-value.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { useControlledValue } from '../use-controlled-value'; | ||
|
||
function Input( props ) { | ||
const [ value, setValue ] = useControlledValue( props ); | ||
return ( | ||
<input | ||
value={ value } | ||
onChange={ ( event ) => setValue( event.target.value ) } | ||
/> | ||
); | ||
} | ||
|
||
function getInput() { | ||
return screen.getByRole( 'textbox' ); | ||
} | ||
|
||
describe( 'useControlledValue', () => { | ||
it( 'should use the default value', () => { | ||
render( <Input defaultValue="WordPress.org" /> ); | ||
expect( getInput() ).toHaveValue( 'WordPress.org' ); | ||
} ); | ||
|
||
it( 'should use the default value then switch to the controlled value', () => { | ||
const { rerender } = render( <Input defaultValue="WordPress.org" /> ); | ||
expect( getInput() ).toHaveValue( 'WordPress.org' ); | ||
|
||
rerender( | ||
<Input defaultValue="WordPress.org" value="Code is Poetry" /> | ||
); | ||
expect( getInput() ).toHaveValue( 'Code is Poetry' ); | ||
} ); | ||
|
||
it( 'should not call onChange only when there is no value being passed in', () => { | ||
const onChange = jest.fn(); | ||
render( <Input defaultValue="WordPress.org" onChange={ onChange } /> ); | ||
|
||
expect( getInput() ).toHaveValue( 'WordPress.org' ); | ||
|
||
fireEvent.change( getInput(), { target: { value: 'Code is Poetry' } } ); | ||
|
||
expect( getInput() ).toHaveValue( 'Code is Poetry' ); | ||
expect( onChange ).not.toHaveBeenCalled(); | ||
} ); | ||
|
||
it( 'should call onChange when there is a value passed in', () => { | ||
const onChange = jest.fn(); | ||
const { rerender } = render( | ||
<Input | ||
defaultValue="WordPress.org" | ||
value="Code is Poetry" | ||
onChange={ onChange } | ||
/> | ||
); | ||
|
||
expect( getInput() ).toHaveValue( 'Code is Poetry' ); | ||
|
||
fireEvent.change( getInput(), { | ||
target: { value: 'WordPress rocks!' }, | ||
} ); | ||
|
||
rerender( | ||
<Input | ||
defaultValue="WordPress.org" | ||
value="WordPress rocks!" | ||
onChange={ onChange } | ||
/> | ||
); | ||
|
||
expect( getInput() ).toHaveValue( 'WordPress rocks!' ); | ||
expect( onChange ).toHaveBeenCalledWith( 'WordPress rocks!' ); | ||
} ); | ||
|
||
it( 'should not maintain internal state if no onChange is passed but a value is passed', () => { | ||
const { rerender } = render( <Input value="Code is Poetry" /> ); | ||
|
||
expect( getInput() ).toHaveValue( 'Code is Poetry' ); | ||
|
||
// primarily this proves that the hook doesn't break if no onChange is passed but | ||
// value turns into a controlled state, for example if the value needs to be set | ||
// to a constant in certain conditions but no change listening needs to happen | ||
fireEvent.change( getInput(), { target: { value: 'WordPress.org' } } ); | ||
|
||
// If `value` is passed then we expect the value to be fully controlled | ||
// meaning that the value passed in will always be used even though | ||
// we're managing internal state. | ||
expect( getInput() ).toHaveValue( 'Code is Poetry' ); | ||
|
||
// Next we un-set the value to uncover the internal state which was still maintained | ||
rerender( <Input /> ); | ||
|
||
expect( getInput() ).toHaveValue( 'WordPress.org' ); | ||
} ); | ||
} ); |
34 changes: 34 additions & 0 deletions
34
packages/components/src/utils/hooks/use-controlled-value.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState } from '@wordpress/element'; | ||
|
||
type Props< T > = { | ||
defaultValue?: T; | ||
value?: T; | ||
onChange?: ( value: T ) => void; | ||
}; | ||
|
||
/** | ||
* Simplified and improved implementation of useControlledState. | ||
* | ||
* @param props | ||
* @param props.defaultValue | ||
* @param props.value | ||
* @param props.onChange | ||
* @return The controlled value and the value setter. | ||
*/ | ||
export function useControlledValue< T >( { | ||
defaultValue, | ||
onChange, | ||
value: valueProp, | ||
}: Props< T > ): [ T | undefined, ( value: T ) => void ] { | ||
const hasValue = typeof valueProp !== 'undefined'; | ||
const initialValue = hasValue ? valueProp : defaultValue; | ||
const [ state, setState ] = useState( initialValue ); | ||
const value = hasValue ? valueProp : state; | ||
const setValue = | ||
hasValue && typeof onChange === 'function' ? onChange : setState; | ||
|
||
return [ value, setValue ]; | ||
} |