- Notifications
You must be signed in to change notification settings - Fork 191
feat: Support usePropState #673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { useState } from 'react'; | ||
| import useLayoutEffect from './useLayoutEffect'; | ||
| | ||
| type Updater<T> = (updater: T | ((origin: T) => T)) => void; | ||
| | ||
| /** | ||
| * Similar to `useState` but will use props value if provided. | ||
| * From React 18, we do not need safe `useState` since it will not throw for unmounted update. | ||
| * This hooks remove the `onChange` & `postState` logic since we only need basic merged state logic. | ||
| */ | ||
| export default function useControlledState<T>( | ||
| defaultStateValue: T | (() => T), | ||
| value?: T, | ||
| ): [T, Updater<T>] { | ||
| const [innerValue, setInnerValue] = useState<T>(defaultStateValue); | ||
| | ||
| const mergedValue = value !== undefined ? value : innerValue; | ||
| | ||
| useLayoutEffect( | ||
| mount => { | ||
| if (!mount) { | ||
| setInnerValue(value); | ||
| } | ||
| }, | ||
| [value], | ||
| ); | ||
| | ||
| return [ | ||
| // Value | ||
| mergedValue, | ||
| // Update function | ||
| setInnerValue, | ||
| ]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| | @@ -8,6 +8,7 @@ import useMergedState from '../src/hooks/useMergedState'; | |||||||||||||||||||||||||||||||||
| import useMobile from '../src/hooks/useMobile'; | ||||||||||||||||||||||||||||||||||
| import useState from '../src/hooks/useState'; | ||||||||||||||||||||||||||||||||||
| import useSyncState from '../src/hooks/useSyncState'; | ||||||||||||||||||||||||||||||||||
| import useControlledState from '../src/hooks/useControlledState'; | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| global.disableUseId = false; | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| | @@ -317,6 +318,163 @@ describe('hooks', () => { | |||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| describe('useControlledState', () => { | ||||||||||||||||||||||||||||||||||
| const FC: React.FC<{ | ||||||||||||||||||||||||||||||||||
| value?: string; | ||||||||||||||||||||||||||||||||||
| defaultValue?: string | (() => string); | ||||||||||||||||||||||||||||||||||
| }> = props => { | ||||||||||||||||||||||||||||||||||
| const { value, defaultValue } = props; | ||||||||||||||||||||||||||||||||||
| const [val, setVal] = useControlledState<string>( | ||||||||||||||||||||||||||||||||||
| defaultValue ?? null, | ||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||
| value={val} | ||||||||||||||||||||||||||||||||||
| onChange={e => { | ||||||||||||||||||||||||||||||||||
| setVal(e.target.value); | ||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| <span className="txt">{val}</span> | ||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| it('still control of to undefined', () => { | ||||||||||||||||||||||||||||||||||
| const { container, rerender } = render(<FC value="test" />); | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| expect(container.querySelector('input').value).toEqual('test'); | ||||||||||||||||||||||||||||||||||
| expect(container.querySelector('.txt').textContent).toEqual('test'); | ||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||
| rerender(<FC value={undefined} />); | ||||||||||||||||||||||||||||||||||
| expect(container.querySelector('input').value).toEqual('test'); | ||||||||||||||||||||||||||||||||||
| expect(container.querySelector('.txt').textContent).toEqual(''); | ||||||||||||||||||||||||||||||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个断言似乎不正确。 Suggested change
| ||||||||||||||||||||||||||||||||||
| expect(container.querySelector('.txt').textContent).toEqual(''); | |
| expect(container.querySelector('.txt').textContent).toEqual('test'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
修复 Biome 报错:Unexpected empty array pattern
空数组解构会触发 lint/correctness/noEmptyPattern。这里无需接收返回值,直接调用即可。
- const [] = useControlledState(undefined); + useControlledState(undefined);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [] = useControlledState(undefined); | |
| count += 1; | |
| return null; | |
| }; | |
| render(<Demo />); | |
| expect(count).toBe(1); | |
| }); | |
| useControlledState(undefined); | |
| count += 1; | |
| return null; | |
| }; | |
| render(<Demo />); | |
| expect(count).toBe(1); | |
| }); |
🧰 Tools
🪛 Biome (2.1.2)
[error] 468-468: Unexpected empty array pattern.
(lint/correctness/noEmptyPattern)
🤖 Prompt for AI Agents
In tests/hooks.test.tsx around lines 468 to 475, the line using an empty array destructuring "const [] = useControlledState(undefined);" triggers Biome lint rule noEmptyPattern; remove the empty destructuring and simply call useControlledState(undefined) directly (i.e., replace the const [] = ... with a plain call), so the hook is invoked without assigning its return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
valueisundefinedanddefaultStateValueis a function, the function will be called on every render instead of only on initialization. This could cause performance issues and unexpected behavior. Consider using a lazy initial state pattern or memoization.