Skip to content

Commit 8842eaa

Browse files
author
Gijs Nieuwenhuis
committed
Added useStateUntilUnmount React hook
1 parent 90aa0ae commit 8842eaa

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ npm install git+ssh://git@github.com:freshheads/freshheads-javascript-essentials
3838
- [`useScrollToTopOnDependencyChange`](#usescrolltotopondependencychange)
3939
- [`useTrackingProps`](#usetrackingprops)
4040
- [`usePromiseEffect`](#usepromiseeffect)
41+
- [`useStateUntilUnmount`](#usestateuntilunmount)
4142
- [Storage](#storage)
4243
- [`localStorage`](#localstorage)
4344
- [`sessionStorage`](#sessionstorage)
@@ -494,6 +495,46 @@ const ArticleOverview: React.FC<Props> => ({ page }) => {
494495
}
495496
```
496497
498+
### `useStateUntilUnmount`
499+
500+
We have all seen the warning below popup sometimes:
501+
502+
> Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks...
503+
504+
We often execute asynchronous actions (i.e. API calls) that, when finished, update some component state. When however the component that the action belongs to, is unmounted in the meantime, the no longer needed state (!) is still updated, causing the warning above. Some sort of reference to the component needs to remain in memory to allow the state change to occur, which is a memory leak in your application.
505+
506+
This hook ensures that, once the component is unmounted, the no longer required component state is not updated, making sure that the warning is not triggered.
507+
508+
If it actually fixes the memory leak, [remains to be seen](https://gist.github.com/troygoode/0702ebabcf3875793feffe9b65da651a#gistcomment-3662958), and usage of this hook is only preferred when there is not a better solution available (or affordable), like awaiting unmount until the async action is finished. Use with care..
509+
510+
Usage:
511+
512+
```typescript jsx
513+
import React, { useEffect } from 'react';
514+
import useStateUntilUnmount from '@freshheads/javascript-essentials/build/react/hooks/useStateUntilUnmount'
515+
516+
type Props = {
517+
slug: string;
518+
}
519+
520+
const SomeComponent: React.VFC = ({ slug }) => {
521+
const [isFetching, setIsFetching] = useStateUntilUnmount<boolean>(false);
522+
523+
useEffect(() => {
524+
setIsFetching(true);
525+
526+
fetchArticleWithSlug(slug).finally(() => {
527+
// normally, when this React component is unmounted, before we get
528+
// to this point, the React warning above would popup.
529+
530+
setIsFetching(false);
531+
})
532+
}, [slug]);
533+
534+
// ...
535+
}
536+
```
537+
497538
## Routing
498539
499540
### `createPathFromRoute`
@@ -619,7 +660,6 @@ toJson({ value: new SomeClass() }); // = typescript error
619660
620661
# Todo
621662
622-
- [hook that makes sure that no state updated when the component is already unmounted](https://gist.github.com/troygoode/0702ebabcf3875793feffe9b65da651a)
623663
- [Money formatting](https://github.com/freshheads/013/blob/develop/assets/frontend/src/js/utility/numberUtilities.ts)
624664
- [Tracking utilities](https://github.com/freshheads/013/blob/develop/assets/frontend/src/js/utility/trackingUtilities.ts) (misschien ook HOC oid. `withTrackingOnClick` oid.? Of een hook?)
625665
- [Routing: extract path with placeholders](https://github.com/freshheads/013/blob/develop/assets/frontend/src/js/routing/utility/urlGenerator.ts#L13)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import useStateUntilUnmount from '../useStateUntilUnmount';
3+
4+
describe('useStateUntilUnmount', () => {
5+
it('should be usable as a regular `useState()` hook', () => {
6+
const initialValue = 'some value';
7+
8+
const { result } = renderHook(() =>
9+
useStateUntilUnmount<string>('some value')
10+
);
11+
12+
const [initialState] = result.current;
13+
14+
expect(initialState).toBe(initialValue);
15+
16+
const updatedValue = 'other value';
17+
18+
act(() => {
19+
const [, setState] = result.current;
20+
21+
setState(updatedValue);
22+
});
23+
24+
const [updatedState] = result.current;
25+
26+
expect(updatedState).toBe(updatedValue);
27+
});
28+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
2+
3+
export default function useStateUntilUnmount<T>(
4+
initialValue: T
5+
): [T, Dispatch<SetStateAction<T>>] {
6+
// useRef to memorize if the component is mounted between renders
7+
const isMounted = useRef<boolean>(false);
8+
9+
const [value, setValueState] = useState<T>(initialValue);
10+
11+
useEffect(() => {
12+
isMounted.current = true;
13+
14+
return () => {
15+
isMounted.current = false;
16+
};
17+
});
18+
19+
const setValue = useRef<Dispatch<SetStateAction<T>>>((...args) => {
20+
if (!isMounted.current) {
21+
return;
22+
}
23+
24+
setValueState(...args);
25+
});
26+
27+
return [value, setValue.current];
28+
}

0 commit comments

Comments
 (0)