Skip to content

Commit 94b8c7f

Browse files
author
Gijs Nieuwenhuis
committed
Added usePromiseEffect() React hook
1 parent a138f79 commit 94b8c7f

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ A library containing javascript utilities that we until now often copy between p
3030
- [`useStateWithRef`](#usestatewithref)
3131
- [`useScrollToTopOnDependencyChange`](#usescrolltotopondependencychange)
3232
- [`useTrackingProps`](#usetrackingprops)
33+
- [`usePromiseEffect`](#usepromiseeffect)
3334
- [Storage](#storage)
3435
- [`localStorage`](#localstorage)
3536
- [`sessionStorage`](#sessionstorage)
@@ -425,6 +426,50 @@ const PreorderSubmitButton: React.FC<Props> = (subscribeToNewsletter) => {
425426
};
426427
```
427428
429+
### `usePromiseEffect`
430+
431+
When working with promises in `useEffect()` you have many things to take into account:
432+
433+
- What to render when the promise is pending
434+
- How to catch errors and render your error notification
435+
- What to render when the value is resolved
436+
437+
This hook helps you with the status changes and reduces the number of re-renders required to get there.
438+
439+
Usage:
440+
441+
```tsx
442+
import usePromiseEffect from '@freshheads/javascript-essentials/build/react/hooks/usePromiseEffect';
443+
444+
type Props = {
445+
page: number
446+
}
447+
448+
const ArticleOverview: React.FC<Props> => ({ page }) => {
449+
const { value: articles, pending, error } = usePromiseEffect<Article[]>(() => fetchArticles(), [page]);
450+
451+
if (pending) {
452+
return <Loader />;
453+
}
454+
455+
if (error) {
456+
return <Error message={error.message} />;
457+
}
458+
459+
if (!value) {
460+
throw new Error('Value should be available at this point');
461+
}
462+
463+
return (
464+
<div>
465+
{ state.value.map(article => (
466+
<Article data={article} key={article.id} />
467+
)) }
468+
</div>
469+
)
470+
}
471+
```
472+
428473
## Routing
429474
430475
### `createPathFromRoute`
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { EffectExecutor } from './../usePromiseEffect';
2+
import { renderHook, act } from '@testing-library/react-hooks';
3+
import usePromiseEffect, { Status, Response } from '../usePromiseEffect';
4+
5+
describe('usePromiseEffect', () => {
6+
describe('When pending', () => {
7+
it('should return the pending state', (done) => {
8+
const executeEffect: EffectExecutor<string> = async () => {
9+
return new Promise((resolve) => {
10+
setTimeout(() => {
11+
resolve('hallo');
12+
}, 200);
13+
});
14+
};
15+
16+
const { result } = renderHook<{}, Response<string>>(() =>
17+
usePromiseEffect<string>(executeEffect, [])
18+
);
19+
20+
expect(result.current).toEqual({
21+
pending: true,
22+
error: null,
23+
value: null,
24+
});
25+
26+
done();
27+
});
28+
});
29+
30+
describe('When successful', () => {
31+
it('should return the fulffilled state', async (done) => {
32+
const executeEffect: EffectExecutor<string> = async () => {
33+
return new Promise((resolve) => {
34+
setTimeout(() => {
35+
resolve('hallo');
36+
}, 200);
37+
});
38+
};
39+
40+
const { result, waitForNextUpdate } = renderHook<
41+
{},
42+
Response<string>
43+
>(() => usePromiseEffect<string>(executeEffect, []));
44+
45+
await waitForNextUpdate();
46+
47+
expect(result.current).toEqual({
48+
pending: false,
49+
value: 'hallo',
50+
error: null,
51+
});
52+
53+
done();
54+
});
55+
});
56+
57+
describe('When an error occurs', () => {
58+
it('should return the error state', async (done) => {
59+
const error = new Error('Some test error');
60+
61+
const executeEffect: EffectExecutor<string> = async () => {
62+
return new Promise((_resolve, reject) => {
63+
setTimeout(() => {
64+
reject(error);
65+
}, 200);
66+
});
67+
};
68+
69+
const { result, waitForNextUpdate } = renderHook<
70+
{},
71+
Response<string>
72+
>(() => usePromiseEffect<string>(executeEffect, []));
73+
74+
await waitForNextUpdate();
75+
76+
expect(result.current).toEqual({
77+
error,
78+
value: null,
79+
pending: false,
80+
});
81+
82+
done();
83+
});
84+
});
85+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { DependencyList, useEffect, useState } from 'react';
2+
3+
export enum Status {
4+
Pending = 'pending',
5+
Fulfilled = 'fullfiled',
6+
Rejected = 'rejected',
7+
}
8+
9+
export type PendingState = { status: Status.Pending; value: null; error: null };
10+
11+
export type RejectedState = {
12+
status: Status.Rejected;
13+
value: null;
14+
error: Error;
15+
};
16+
17+
export type FulfilledState<T> = {
18+
status: Status.Fulfilled;
19+
value: T;
20+
error: null;
21+
};
22+
23+
export type PromiseState<T> = PendingState | FulfilledState<T> | RejectedState;
24+
25+
export type EffectExecutor<T> = () => Promise<T>;
26+
27+
export type Response<T> = {
28+
value: T | null;
29+
pending: boolean;
30+
error: Error | null;
31+
};
32+
33+
export default function usePromiseEffect<T>(
34+
executeEffect: EffectExecutor<T>,
35+
deps: DependencyList,
36+
cleanup?: () => void
37+
): Response<T> {
38+
const [state, setState] = useState<PromiseState<T>>({
39+
status: Status.Pending,
40+
value: null,
41+
error: null,
42+
});
43+
44+
useEffect(() => {
45+
executeEffect()
46+
.then((value) => {
47+
setState({
48+
status: Status.Fulfilled,
49+
value,
50+
error: null,
51+
});
52+
})
53+
.catch((error) =>
54+
setState({ status: Status.Rejected, value: null, error })
55+
);
56+
57+
return cleanup;
58+
}, deps);
59+
60+
return {
61+
value: state.value,
62+
pending: state.status === Status.Pending,
63+
error: state.error,
64+
};
65+
}

0 commit comments

Comments
 (0)