Skip to content

Commit eca4dd1

Browse files
committed
chore: comment
1 parent 2880228 commit eca4dd1

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

src/hooks/useMergedState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function hasValue(value: any) {
1313
}
1414

1515
/**
16+
* @deprecated Please use `usePropState` instead if not need support < React 18.
1617
* Similar to `useState` but will use props value if provided.
1718
* Note that internal use rc-util `useState` hook.
1819
*/

src/hooks/usePropState.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useState } from 'react';
2+
import useLayoutEffect from './useLayoutEffect';
3+
4+
type Updater<T> = (updater: T | ((origin: T) => T)) => void;
5+
6+
/**
7+
* Similar to `useState` but will use props value if provided.
8+
* From React 18, we do not need safe `useState` since it will not throw for unmounted update.
9+
* This hooks remove the `onChange` & `postState` logic since we only need basic merged state logic.
10+
*/
11+
export default function usePropState<T>(
12+
defaultStateValue: T | (() => T),
13+
value?: T,
14+
): [T, Updater<T>] {
15+
const [innerValue, setInnerValue] = useState<T>(defaultStateValue);
16+
17+
const mergedValue = value !== undefined ? value : innerValue;
18+
19+
useLayoutEffect(
20+
mount => {
21+
if (!mount) {
22+
setInnerValue(value);
23+
}
24+
},
25+
[value],
26+
);
27+
28+
return [
29+
// Value
30+
mergedValue,
31+
// Update function
32+
setInnerValue,
33+
];
34+
}

tests/hooks.test.tsx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import useMergedState from '../src/hooks/useMergedState';
88
import useMobile from '../src/hooks/useMobile';
99
import useState from '../src/hooks/useState';
1010
import useSyncState from '../src/hooks/useSyncState';
11+
import usePropState from '../src/hooks/usePropState';
1112

1213
global.disableUseId = false;
1314

@@ -317,6 +318,160 @@ describe('hooks', () => {
317318
});
318319
});
319320

321+
describe('usePropState', () => {
322+
const FC: React.FC<{
323+
value?: string;
324+
defaultValue?: string | (() => string);
325+
}> = props => {
326+
const { value, defaultValue } = props;
327+
const [val, setVal] = usePropState<string>(defaultValue ?? null, value);
328+
return (
329+
<>
330+
<input
331+
value={val}
332+
onChange={e => {
333+
setVal(e.target.value);
334+
}}
335+
/>
336+
<span className="txt">{val}</span>
337+
</>
338+
);
339+
};
340+
341+
it('still control of to undefined', () => {
342+
const { container, rerender } = render(<FC value="test" />);
343+
344+
expect(container.querySelector('input').value).toEqual('test');
345+
expect(container.querySelector('.txt').textContent).toEqual('test');
346+
347+
rerender(<FC value={undefined} />);
348+
expect(container.querySelector('input').value).toEqual('test');
349+
expect(container.querySelector('.txt').textContent).toEqual('');
350+
});
351+
352+
describe('correct defaultValue', () => {
353+
it('raw', () => {
354+
const { container } = render(<FC defaultValue="test" />);
355+
356+
expect(container.querySelector('input').value).toEqual('test');
357+
});
358+
359+
it('func', () => {
360+
const { container } = render(<FC defaultValue={() => 'bamboo'} />);
361+
362+
expect(container.querySelector('input').value).toEqual('bamboo');
363+
});
364+
});
365+
366+
it('not rerender when setState as deps', () => {
367+
let renderTimes = 0;
368+
369+
const Test = () => {
370+
const [val, setVal] = usePropState(0);
371+
372+
React.useEffect(() => {
373+
renderTimes += 1;
374+
expect(renderTimes < 10).toBeTruthy();
375+
376+
setVal(1);
377+
}, [setVal]);
378+
379+
return <div>{val}</div>;
380+
};
381+
382+
const { container } = render(<Test />);
383+
expect(container.firstChild.textContent).toEqual('1');
384+
});
385+
386+
it('React 18 should not reset to undefined', () => {
387+
const Demo = () => {
388+
const [val] = usePropState(33, undefined);
389+
390+
return <div>{val}</div>;
391+
};
392+
393+
const { container } = render(
394+
<React.StrictMode>
395+
<Demo />
396+
</React.StrictMode>,
397+
);
398+
399+
expect(container.querySelector('div').textContent).toEqual('33');
400+
});
401+
402+
it('uncontrolled to controlled', () => {
403+
const Demo: React.FC<Readonly<{ value?: number }>> = ({ value }) => {
404+
const [mergedValue, setMergedValue] = usePropState<number>(
405+
() => 233,
406+
value,
407+
);
408+
409+
return (
410+
<span
411+
onClick={() => {
412+
setMergedValue(v => v + 1);
413+
setMergedValue(v => v + 1);
414+
}}
415+
onMouseEnter={() => {
416+
setMergedValue(1);
417+
}}
418+
>
419+
{mergedValue}
420+
</span>
421+
);
422+
};
423+
424+
const { container, rerender } = render(<Demo />);
425+
expect(container.textContent).toEqual('233');
426+
427+
// Update value
428+
rerender(<Demo value={1} />);
429+
expect(container.textContent).toEqual('1');
430+
431+
// Click update
432+
rerender(<Demo value={undefined} />);
433+
fireEvent.mouseEnter(container.querySelector('span'));
434+
fireEvent.click(container.querySelector('span'));
435+
expect(container.textContent).toEqual('3');
436+
});
437+
438+
it('should alway use option value', () => {
439+
const Test: React.FC<Readonly<{ value?: number }>> = ({ value }) => {
440+
const [mergedValue, setMergedValue] = usePropState<number>(
441+
undefined,
442+
value,
443+
);
444+
return (
445+
<span
446+
onClick={() => {
447+
setMergedValue(12);
448+
}}
449+
>
450+
{mergedValue}
451+
</span>
452+
);
453+
};
454+
455+
const { container } = render(<Test value={1} />);
456+
fireEvent.click(container.querySelector('span'));
457+
458+
expect(container.textContent).toBe('1');
459+
});
460+
461+
it('render once', () => {
462+
let count = 0;
463+
464+
const Demo: React.FC = () => {
465+
const [] = usePropState(undefined);
466+
count += 1;
467+
return null;
468+
};
469+
470+
render(<Demo />);
471+
expect(count).toBe(1);
472+
});
473+
});
474+
320475
describe('useLayoutEffect', () => {
321476
const FC: React.FC<Readonly<{ defaultValue?: string }>> = props => {
322477
const { defaultValue } = props;

0 commit comments

Comments
 (0)