Skip to content

Commit ad80e80

Browse files
committed
[FIX] various
1 parent 0ee4fa6 commit ad80e80

23 files changed

Lines changed: 388 additions & 41 deletions

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ Go to [Demo](https://react-tools.ndria.dev) to see and try all implementations.
6666
- [__LIFECYCLE__](#lifecycle)
6767

6868
- [_useDeferredValue_](#useDeferredValue)
69+
- [_useEffectAbortable_](#useEffectAbortable)
6970
- [_useEffectCompare_](#useEffectCompare)
7071
- [_useEffectDeepCompare_](#useEffectDeepCompare)
7172
- [_useEffectOnce_](#useEffectOnce)
7273
- [_useIsMounted_](#useIsMounted)
74+
- [_useLayoutEffectAbortable_](#useLayoutEffectAbortable)
7375
- [_useLayoutEffectCompare_](#useLayoutEffectCompare)
7476
- [_useLayoutEffectDeepCompare_](#useLayoutEffectDeepCompare)
7577
- [_useLayoutEffectOnce_](#useLayoutEffectOnce)
@@ -296,7 +298,7 @@ useSet<T>(initialState?: Iterable<T> | (() => Iterable<T>))
296298
297299
Custom useState with get and reset state functions. [See demo](https://react-tools.ndria.dev/#/hooks/state/useStateGetReset)
298300
```tsx
299-
useStateGetReset<T>(initialState?: T | (() => T)): [T, Dispatch<SetStateAction<T>>, () => T, () => void] | [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void]
301+
useStateGetReset<T>(initialState?: T | (() => T) | undefined): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void] | [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void]
300302
```
301303
302304
### useStateHistory
@@ -336,6 +338,13 @@ _useDeferredValue_ hook polyfilled for React versions below 18. [See demo](https
336338
useDeferredValue<T>(value: T): T
337339
```
338340
341+
### useEffectAbortable
342+
343+
Custom useEffect with a unified cancellation mechanism to ensure complete cleanup and to prevent the warning that appears on old React version _"Can't perform a React state update on an unmounted component"_. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useEffectAbortable)
344+
```tsx
345+
useEffectAbortable<T = unknown>(cb: (signal: AbortSignal) => void | Promise<void> | (() => void) | (Promise<() => void>), deps: DependencyListTyped<T>)
346+
```
347+
339348
### useEffectCompare
340349
341350
Custom useEffect that reexecutes EffectCallback only when comparator function, received as third parameter, returns true. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useEffectCompare)
@@ -364,6 +373,13 @@ Hoos to know when a component is mounted or not. [See demo](https://react-tools.
364373
useIsMounted(): ()=>boolean
365374
```
366375
376+
### useLayoutEffectAbortable
377+
378+
Custom useLayoutEffect with a unified cancellation mechanism to ensure complete cleanup and to prevent the warning that appears on old React version _"Can't perform a React state update on an unmounted component"_. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useEffectAbortable)
379+
```tsx
380+
useLayoutEffectAbortable<T = unknown>(cb: (signal: AbortSignal) => void | Promise<void> | (() => void) | (Promise<() => void>), deps: DependencyListTyped<T>)
381+
```
382+
367383
### useLayoutEffectCompare
368384
369385
Custom useLayoutEffect that reexecutes EffectCallback only when comparator function, received as third parameter, returns true. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useLayoutEffectCompare)

apps/react-tools-demo/src/layout/MainLayout.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@ export default function MainLayout() {
7070
<option value="useStateValidator"></option>
7171
<option value="useSyncExternalStore"></option>
7272
<option value="useDeferredValue"></option>
73+
<option value="useEffectAbortable"></option>
7374
<option value="useEffectCompare"></option>
7475
<option value="useEffectDeepCompare"></option>
7576
<option value="useEffectOnce"></option>
7677
<option value="useIsMounted"></option>
78+
<option value="useLayoutEffectAbortable"></option>
7779
<option value="useLayoutEffectCompare"></option>
7880
<option value="useLayoutEffectDeepCompare"></option>
7981
<option value="useLayoutEffectOnce"></option>
@@ -435,6 +437,19 @@ export default function MainLayout() {
435437
>
436438
useDeferredValue
437439
</Link>
440+
<Link
441+
ref={node => linksRef.current["useEffectAbortable"] = node}
442+
to="/hooks/lifecycle/useEffectAbortable"
443+
onClick={() => {
444+
Object.values(linksRef.current).forEach(l => l?.classList.remove("active"));
445+
linksRef.current["useEffectAbortable"]?.classList.add("active");
446+
containerRef.current?.scrollTo(0, 0);
447+
window.innerWidth < 1190 && closeNav();
448+
}}
449+
translate="no"
450+
>
451+
useEffectAbortable
452+
</Link>
438453
<Link
439454
ref={node => linksRef.current["useEffectCompare"] = node}
440455
to="/hooks/lifecycle/useEffectCompare"
@@ -487,6 +502,19 @@ export default function MainLayout() {
487502
>
488503
useIsMounted
489504
</Link>
505+
<Link
506+
ref={node => linksRef.current["useLayoutEffectAbortable"] = node}
507+
to="/hooks/lifecycle/useLayoutEffectAbortable"
508+
onClick={() => {
509+
Object.values(linksRef.current).forEach(l => l?.classList.remove("active"));
510+
linksRef.current["useLayoutEffectAbortable"]?.classList.add("active");
511+
containerRef.current?.scrollTo(0, 0);
512+
window.innerWidth < 1190 && closeNav();
513+
}}
514+
translate="no"
515+
>
516+
useLayoutEffectAbortable
517+
</Link>
490518
<Link
491519
ref={node => linksRef.current["useLayoutEffectCompare"] = node}
492520
to="/hooks/lifecycle/useLayoutEffectCompare"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# useEffectAbortable
2+
Custom useEffect with a unified cancellation mechanism to ensure complete cleanup and to prevent the warning that appears on old React version _"Can't perform a React state update on an unmounted component"_. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useEffectAbortable)
3+
4+
## Usage
5+
6+
```tsx
7+
const Demo = ({ setDate }: {setDate: (cb: (n:number)=> number) => void}) => {
8+
const [state, setState] = useState("IDLE");
9+
10+
useEffectAbortable((signal) => {
11+
let id = setTimeout(() => {
12+
setState("LOADING");
13+
}, 1000);
14+
let id1 = setTimeout(() => {
15+
setState("SUCCESS");
16+
setDate(d => d+1);
17+
}, 5000)
18+
signal.addEventListener('abort', () => {
19+
clearTimeout(id);
20+
clearTimeout(id1);
21+
});
22+
}, []);
23+
24+
return <p>
25+
{state}
26+
</p>
27+
28+
}
29+
const UseEffectAbortable = () => {
30+
const [state, setState] = useState(false);
31+
const [data, setData] = useState(0);
32+
33+
return (<>
34+
<p>Current state: {state ? "MOUNT" : "UNMOUNT"}</p>
35+
<p>Current data: {data}</p>
36+
<button onClick={() => setState(t => !t)}>{state ? "Unmount" : "Mount"}</button>
37+
<Show when={state}>
38+
<Demo setDate={setData}/>
39+
</Show>
40+
</>);
41+
};
42+
43+
UseEffectAbortable.displayName = "UseEffectAbortable";
44+
45+
export { UseEffectAbortable };
46+
```
47+
48+
> The component has:
49+
> - a _state useState variable_ boolean uaws by a Show component to render the _Demo component_.
50+
> - a _data useState variable_ with value 0.
51+
> - a button that toggle _state between __false__ and __true__ value.
52+
> - a _Demo component_ that receives the update function for _data variable_. It has a _useEffectAbortable_ that hat simulates an asynchronous call during which a loading message is shown and at the end of which increment by one _date varibale_.
53+
>
54+
> If you click the _mount button_ the _Demo component_ is rendering. The simulated call starts and at the end updates the _date variable_ but if the _unmount button_ is clicked before call ending, it will be canceled with the _useEffectAbortable_ and _date variable_ will not be updated.
55+
56+
57+
## API
58+
59+
```tsx
60+
useEffectAbortable<T = unknown>(cb: (signal: AbortSignal) => void | Promise<void> | (() => void) | (Promise<() => void>), deps: DependencyListTyped<T>)
61+
```
62+
63+
64+
> ### Params
65+
>
66+
> - __cb__: _(signal: AbortSignal)=> void | Promise<void> | (() => void) | (Promise<() => void>)_
67+
Imperative function with abort signal parameter that can return a cleanup function.
68+
> - __deps__: _DependencyListTyped_
69+
typed dependency list.
70+
>
71+
72+
73+
74+
> ### Returns
75+
>
76+
>
77+
> - _void_
78+
>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# useLayoutEffectAbortable
2+
Custom useLayoutEffect with a unified cancellation mechanism to ensure complete cleanup and to prevent the warning that appears on old React version _"Can't perform a React state update on an unmounted component"_. [See demo](https://react-tools.ndria.dev/#/hooks/lifecycle/useEffectAbortable)
3+
4+
## Usage
5+
6+
The implementation is like that _useEffectAbortable_.
7+
8+
Please visit [useEffectAbortable](#/hooks/lifecycle/useEffectAbortable) example to see how it works.
9+
10+
## API
11+
12+
```tsx
13+
useLayoutEffectAbortable<T = unknown>(cb: (signal: AbortSignal) => void | Promise<void> | (() => void) | (Promise<() => void>), deps: DependencyListTyped<T>)
14+
```
15+
16+
17+
> ### Params
18+
>
19+
> - __cb__: _(signal: AbortSignal)=> void | Promise<void> | (() => void) | (Promise<() => void>)_
20+
Imperative function with abort signal parameter that can return a cleanup function.
21+
> - __deps__: _DependencyListTyped_
22+
typed dependency list.
23+
>
24+
25+
26+
27+
> ### Returns
28+
>
29+
>
30+
> - _void_
31+
>

apps/react-tools-demo/src/markdown/useStateGetReset.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const UseStateGetReset = () => {
4242
<Input id="eta" name="eta" value={state.eta} onChange={onChange} />
4343
</div>
4444
</div>
45+
<Demo setS={setState as unknown as ReturnType<typeof useStateGetReset>[1]} getS={getState} reset={resetState} />
4546
</div>
4647
);
4748
};
@@ -65,7 +66,7 @@ export { UseStateGetReset };
6566
## API
6667

6768
```tsx
68-
useStateGetReset<T>(initialState?: T | (() => T)): [T, Dispatch<SetStateAction<T>>, () => T, () => void] | [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void]
69+
useStateGetReset<T>(initialState?: T | (() => T) | undefined): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void] | [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined, () => void]
6970
```
7071

7172

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState } from "react";
2+
import { useEffectAbortable } from "../../../../../../../packages/react-tools-lib/src/hooks";
3+
import { Show } from "../../../../../../../packages/react-tools-lib/src";
4+
5+
/**
6+
The component has:
7+
- a _state useState variable_ boolean uaws by a Show component to render the _Demo component_.
8+
- a _data useState variable_ with value 0.
9+
- a button that toggle _state between __false__ and __true__ value.
10+
- a _Demo component_ that receives the update function for _data variable_. It has a _useEffectAbortable_ that hat simulates an asynchronous call during which a loading message is shown and at the end of which increment by one _date varibale_.
11+
12+
If you click the _mount button_ the _Demo component_ is rendering. The simulated call starts and at the end updates the _date variable_ but if the _unmount button_ is clicked before call ending, it will be canceled with the _useEffectAbortable_ and _date variable_ will not be updated.
13+
*/
14+
const Demo = ({ setDate }: {setDate: (cb: (n:number)=> number) => void}) => {
15+
const [state, setState] = useState("IDLE");
16+
17+
useEffectAbortable((signal) => {
18+
let id = setTimeout(() => {
19+
setState("LOADING");
20+
}, 1000);
21+
let id1 = setTimeout(() => {
22+
setState("SUCCESS");
23+
setDate(d => d+1);
24+
}, 5000)
25+
signal.addEventListener('abort', () => {
26+
clearTimeout(id);
27+
clearTimeout(id1);
28+
});
29+
}, []);
30+
31+
return <p>
32+
{state}
33+
</p>
34+
35+
}
36+
const UseEffectAbortable = () => {
37+
const [state, setState] = useState(false);
38+
const [data, setData] = useState(0);
39+
40+
return (<>
41+
<p>Current state: {state ? "MOUNT" : "UNMOUNT"}</p>
42+
<p>Current data: {data}</p>
43+
<button onClick={() => setState(t => !t)}>{state ? "Unmount" : "Mount"}</button>
44+
<Show when={state}>
45+
<Demo setDate={setData}/>
46+
</Show>
47+
</>);
48+
};
49+
50+
UseEffectAbortable.displayName = "UseEffectAbortable";
51+
52+
export { UseEffectAbortable };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The implementation is like that _useEffectAbortable_.
2+
3+
Please visit [useEffectAbortable](#/hooks/lifecycle/useEffectAbortable) example to see how it works.

apps/react-tools-demo/src/pages/hooks/state/useStateGetReset/UseStateGetReset.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { BaseSyntheticEvent, useCallback, useState } from 'react';
1+
import { BaseSyntheticEvent, memo, useCallback, useState } from 'react';
22
import { useStateGetReset } from "../../../../../../../packages/react-tools-lib/src";
33
import { Input } from './InputMemo';
44

5+
const Demo = memo(({ setS, getS, reset }: { setS: ReturnType<typeof useStateGetReset>[1], getS: ReturnType<typeof useStateGetReset>[2], reset: ReturnType<typeof useStateGetReset>[3] }) => {
6+
console.log("rerender");
7+
const ss = () => setS || getS() || reset();
8+
return <>
9+
<p>DEMO</p>
10+
</>
11+
})
12+
513
/**
614
The component has:
715
- A _useStateGetReset_ that receives an object, with three properties _id_, _name_, _eta_, and returns a tuple with _stateG_, _setterG_, _getter_ and _resetter_.
@@ -51,6 +59,7 @@ const UseStateGetReset = () => {
5159
<Input id="eta" name="eta" value={state.eta} onChange={onChange} />
5260
</div>
5361
</div>
62+
<Demo setS={setState as unknown as ReturnType<typeof useStateGetReset>[1]} getS={getState} reset={resetState} />
5463
</div>
5564
);
5665
};

apps/react-tools-demo/src/router/Router.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import useDisplayMediaMD from "../markdown/useDisplayMedia.md?raw"
5555
import useDocumentPIPMD from "../markdown/useDocumentPIP.md?raw"
5656
import useDocumentVisibilityMD from "../markdown/useDocumentVisibility.md?raw"
5757
import useDoubleClickMD from "../markdown/useDoubleClick.md?raw"
58+
import useEffectAbortableMD from "../markdown/useEffectAbortable.md?raw"
5859
import useEffectCompareMD from "../markdown/useEffectCompare.md?raw"
5960
import useEffectDeepCompareMD from "../markdown/useEffectDeepCompare.md?raw"
6061
import useEffectOnceMD from "../markdown/useEffectOnce.md?raw"
@@ -76,6 +77,7 @@ import useIntersectionObserverMD from "../markdown/useIntersectionObserver.md?ra
7677
import useIntervalMD from "../markdown/useInterval.md?raw"
7778
import useIsMountedMD from "../markdown/useIsMounted.md?raw"
7879
import useIsOnlineMD from "../markdown/useIsOnline.md?raw"
80+
import useLayoutEffectAbortableMD from "../markdown/useLayoutEffectAbortable.md?raw"
7981
import useLayoutEffectCompareMD from "../markdown/useLayoutEffectCompare.md?raw"
8082
import useLayoutEffectDeepCompareMD from "../markdown/useLayoutEffectDeepCompare.md?raw"
8183
import useLayoutEffectOnceMD from "../markdown/useLayoutEffectOnce.md?raw"
@@ -218,6 +220,7 @@ const UseScreen = lazy((() => import('../pages/hooks/events/useScreen/UseScreen'
218220
const UseScrollIntoView = lazy((() => import('../pages/hooks/events/useScrollIntoView/UseScrollIntoView').then(module => ({default: "default" in module ? module["default"] : module["UseScrollIntoView"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
219221
const UseSwipe = lazy((() => import('../pages/hooks/events/useSwipe/UseSwipe').then(module => ({default: "default" in module ? module["default"] : module["UseSwipe"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
220222
const UseVisible = lazy((() => import('../pages/hooks/events/useVisible/UseVisible').then(module => ({default: "default" in module ? module["default"] : module["UseVisible"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
223+
const UseEffectAbortable = lazy((() => import('../pages/hooks/lifecycle/useEffectAbortable/UseEffectAbortable').then(module => ({default: "default" in module ? module["default"] : module["UseEffectAbortable"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
221224
const UseEffectCompare = lazy((() => import('../pages/hooks/lifecycle/useEffectCompare/UseEffectCompare').then(module => ({default: "default" in module ? module["default"] : module["UseEffectCompare"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
222225
const UseEffectDeepCompare = lazy((() => import('../pages/hooks/lifecycle/useEffectDeepCompare/UseEffectDeepCompare').then(module => ({default: "default" in module ? module["default"] : module["UseEffectDeepCompare"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
223226
const UseEffectOnce = lazy((() => import('../pages/hooks/lifecycle/useEffectOnce/UseEffectOnce').then(module => ({default: "default" in module ? module["default"] : module["UseEffectOnce"]}))) as unknown as () => Promise<{ default: ComponentType; }>)
@@ -395,6 +398,12 @@ function Router() {
395398
<ComponentLayout markdown={useDeferredValueMD} />
396399
</Suspense>
397400
},
401+
{
402+
path: "useEffectAbortable",
403+
element: <Suspense fallback={<Spinner/>}>
404+
<ComponentLayout markdown={useEffectAbortableMD} component={<UseEffectAbortable/>}/>
405+
</Suspense>
406+
},
398407
{
399408
path: "useEffectCompare",
400409
element: <Suspense fallback={<Spinner/>}>
@@ -419,6 +428,12 @@ function Router() {
419428
<ComponentLayout markdown={useIsMountedMD} component={<UseIsMounted/>}/>
420429
</Suspense>
421430
},
431+
{
432+
path: "useLayoutEffectAbortable",
433+
element: <Suspense fallback={<Spinner/>}>
434+
<ComponentLayout markdown={useLayoutEffectAbortableMD} />
435+
</Suspense>
436+
},
422437
{
423438
path: "useLayoutEffectCompare",
424439
element: <Suspense fallback={<Spinner/>}>

0 commit comments

Comments
 (0)