Skip to content

Commit 2cf01a6

Browse files
Added useAsync and useForm
1 parent a7b0007 commit 2cf01a6

File tree

9 files changed

+457
-5
lines changed

9 files changed

+457
-5
lines changed

docs/docs/useAsync.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# useAsync
2+
3+
A hook to manage state of any asynchronous operation. This hook can be used to manage `loading`, `data` and `error` states of an asynchronous operation. It is mainly used for data fetching, the first argument to the hook is the function that fetches and returns data, required options is the second argument and dependencies is the last argument.
4+
5+
Only the most recent promise is considered in this hook, meaning you don't need to worry about any api race conditions could arise because of out of order responses from multiple api calls. In case of error the data will be of previous call or null.
6+
7+
<pre>{`import { useAsync } from 'react-use-custom-hooks';`}</pre>
8+
9+
### Usage example
10+
11+
```typescript
12+
const [data, loading, error, refresh] = useAsync(() => fetchTodo(id), {}, [id]);
13+
```
14+
15+
The fourth return value is a function to execute the operation again forcefully without any dependance change, like a refresh.
16+
17+
```typescript
18+
<button onClick={() => refresh()}>Retry</button>
19+
```
20+
21+
### Playground
22+
23+
The component will fetch and show the todo based on the id, when ever the id changes the component will fetch the todo corresponding to the id. If there is any error in data fetching we will show the error message with a retry button. User can retry the last failed request by clicking the retry button.
24+
25+
```jsx live
26+
function AsyncExample(props) {
27+
const [id, setId] = React.useState(1);
28+
29+
// Move this to a service function
30+
function fetchTodo(id) {
31+
return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
32+
.then(response => response.json())
33+
.then(json => json);
34+
}
35+
36+
// if id changes, data will be fetched for the new id
37+
const [data, loading, error, refresh] = useAsync(() => fetchTodo(id), {}, [
38+
id,
39+
]);
40+
if (loading) return <div>Loading...</div>;
41+
if (error)
42+
return (
43+
<div>
44+
Error: {error.message}
45+
// User can retry on error without page refresh
46+
<button onClick={() => refresh()}>Retry</button>
47+
</div>
48+
);
49+
50+
return (
51+
<>
52+
<p>
53+
Id - <b>{id}</b>
54+
</p>
55+
<p>
56+
Data - <b>{JSON.stringify(data)}</b>
57+
</p>
58+
<button onClick={() => setId(id => id + 1)}>++ Id</button>
59+
<button
60+
style={{ marginLeft: '10px' }}
61+
onClick={() => setId(id => id - 1)}
62+
>
63+
-- Id
64+
</button>
65+
</>
66+
);
67+
}
68+
```
69+
70+
Only the most recent promise is considered in this hook, For example, in the above component there is a chance of race condition as depicted below.
71+
72+
1. Currently id is 123 and fetching details of id is 123
73+
2. Then id changes to 465 while request for 123 is in progress and initiated request for 456
74+
3. Response for 456 came back, state is updated with 456 data
75+
4. Response for 123 came back, state is updated with 123 data
76+
77+
Here the UI will show 123 data even though the user selected 456 as id. This hook will take care of this issue by keeping only the most recent promise in the state by ignoring the current one if a new request is triggered.
78+
79+
### API
80+
81+
```typescript
82+
function useAsync(fn: () => Prm, options?: Options, deps = []);
83+
```
84+
85+
#### Options
86+
87+
| Property | Description | Type | Default |
88+
| --------------- | ---------------------------- | ---------- | ------- |
89+
| successCallback | Callback function on success | `Function` | - |
90+
| errorCallback | Callback function on failure | `Function` | - |

docs/docs/useForm.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# useForm
2+
3+
A hook to manage state of a form. This is an opinionated hook that will manage all internal state of a form and add `isValid` and `errorMessages` properties to each field for validation purpose, these properties are derived from the validator function provided to hook and can used to render error messages and to check if the form field is valid.
4+
5+
<pre>{`import { useForm } from 'react-use-custom-hooks';`}</pre>
6+
7+
### Usage example
8+
9+
```typescript
10+
const [formData, setFormData] = useForm(initialState);
11+
```
12+
13+
1. The initial state passed to this hook should be an object with key is the field name and value is an object with `value` and `validator` properties. The validator function should return `isValid` and `errorMessages` for the particular field upon invoking. For example,
14+
15+
```js
16+
useForm({
17+
name: {
18+
value: '',
19+
validator: value => {
20+
return value.length > 0
21+
? {
22+
isValid: true,
23+
errorMessages: [],
24+
}
25+
: {
26+
isValid: false,
27+
errorMessages: ['Name is required'],
28+
};
29+
},
30+
},
31+
});
32+
```
33+
34+
2. This hook returns two values: form state and form change handler.
35+
3. The form state is an object will have the same structure as initialState with `isValid` and `errorMessages` properties added to every field.
36+
4. The form change handler is a function that takes a field name and a new value and updates the form state.
37+
5. For every call to change handler the validation function will be invoked with new value and form state will be updated accordingly.
38+
39+
### Playground
40+
41+
```jsx live
42+
function FormExample(props) {
43+
const initialState = {
44+
name: {
45+
value: '',
46+
validator: value => {
47+
return value.length > 0
48+
? {
49+
isValid: true,
50+
errorMessages: [],
51+
}
52+
: {
53+
isValid: false,
54+
errorMessages: ['Name is required'],
55+
};
56+
},
57+
},
58+
age: {
59+
value: '',
60+
validator: value => {
61+
if (!value) {
62+
return {
63+
isValid: false,
64+
errorMessages: ['Age is required'],
65+
};
66+
}
67+
if (value > 120) {
68+
return {
69+
isValid: false,
70+
errorMessages: ['Age should be less than 120'],
71+
};
72+
}
73+
if (value < 5) {
74+
return {
75+
isValid: false,
76+
errorMessages: ['Age should be greater than 5'],
77+
};
78+
}
79+
return {
80+
isValid: true,
81+
errorMessages: [],
82+
};
83+
},
84+
},
85+
};
86+
87+
const [formData, setFormData] = useForm(initialState); // All form updating and validation will be taken care by the hook
88+
const { name, age } = formData;
89+
90+
const isValid = Object.values(formData).every(value => value.isValid);
91+
92+
return (
93+
<form>
94+
<label htmlFor="name">Name : </label>
95+
<input
96+
type="text"
97+
value={name.value}
98+
onChange={e => setFormData('name', e.target.value)}
99+
style={{ border: `${!name.isValid ? '2px solid red' : ''}` }}
100+
/>
101+
{name.errorMessages.length > 0 && (
102+
<div style={{ color: 'red' }}>{name.errorMessages[0]}</div>
103+
)}
104+
<br />
105+
<label htmlFor="age">Age : </label>
106+
<input
107+
type="number"
108+
value={age.value}
109+
onChange={e => setFormData('age', e.target.value)}
110+
style={{ border: `${!age.isValid ? '2px solid red' : ''}` }}
111+
/>
112+
{age.errorMessages.length > 0 && (
113+
<div style={{ color: 'red' }}>{age.errorMessages[0]}</div>
114+
)}
115+
<br />
116+
Form is <b>{isValid ? 'valid' : 'invalid'}</b>
117+
</form>
118+
);
119+
}
120+
```
121+
122+
### API
123+
124+
```typescript
125+
function useAsync(fn: () => any, options?: Options, deps = []);
126+
```
127+
128+
#### Options
129+
130+
| Property | Description | Type | Default |
131+
| --------------- | ---------------------------- | ---------- | ------- |
132+
| successCallback | Callback function on success | `Function` | - |
133+
| errorCallback | Callback function on failure | `Function` | - |

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"prism-react-renderer": "^1.2.1",
2424
"react": "^17.0.1",
2525
"react-dom": "^17.0.1",
26-
"react-use-custom-hooks": "^0.2.1"
26+
"react-use-custom-hooks": "^0.3.1"
2727
},
2828
"browserslist": {
2929
"production": [

docs/src/theme/ReactLiveScope/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11

22
import React from 'react';
3-
import { useDebounce, useSafeState, useIsMounted, usePrevious, useLegacyState, useTitle, useIsOnline, useIdle } from "react-use-custom-hooks";
3+
import { useDebounce, useSafeState, useIsMounted, usePrevious, useLegacyState, useTitle, useIsOnline, useIdle, useAsync, useForm } from "react-use-custom-hooks";
44

55
// Add react-live imports you need here
66
const ReactLiveScope = {
77
React,
88
...React,
9-
useDebounce, useSafeState, useIsMounted, usePrevious, useLegacyState, useTitle, useIsOnline, useIdle
9+
useDebounce, useSafeState, useIsMounted, usePrevious, useLegacyState, useTitle, useIsOnline, useIdle, useAsync, useForm
1010
};
1111

1212
export default ReactLiveScope;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.2.1",
2+
"version": "0.3.1",
33
"license": "MIT",
44
"main": "dist/index.js",
55
"typings": "dist/index.d.ts",
@@ -73,4 +73,4 @@
7373
"typescript",
7474
"memory leak"
7575
]
76-
}
76+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { useLegacyState } from './useLegacyState';
66
import { useTitle } from './useTitle';
77
import { useIsOnline } from './useIsOnline';
88
import { useIdle } from './useIdle';
9+
import { useAsync } from './useAsync';
10+
import { useForm } from './useForm';
911

1012
export {
1113
useDebounce,
@@ -16,4 +18,6 @@ export {
1618
useTitle,
1719
useIsOnline,
1820
useIdle,
21+
useAsync,
22+
useForm,
1923
};

src/useAsync.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useEffect, useReducer } from 'react';
2+
3+
interface Options {
4+
successCallback?: Function;
5+
errorCallback?: Function;
6+
}
7+
8+
const initialState = {
9+
loading: true,
10+
data: null,
11+
error: null,
12+
};
13+
14+
function reducer(state: any, action: { type: any; data: any }) {
15+
switch (action.type) {
16+
case 'loading':
17+
return { ...state, loading: action.data };
18+
case 'data':
19+
return { ...state, data: action.data };
20+
case 'error':
21+
return { ...state, error: action.data };
22+
default:
23+
throw new Error();
24+
}
25+
}
26+
27+
function callFunction(fn: Function | undefined, params: any[]) {
28+
if (fn && typeof fn === 'function') {
29+
fn(...params);
30+
}
31+
}
32+
33+
/**
34+
* @param {Function} fn - async function to be called
35+
* @param {object} options - options for the hook
36+
* @param {Function} successCallback - callback to be called when promise resolves
37+
* @param {Function} errorCallback - callback to be called when promise rejects
38+
* @param {Array} deps array of dependencies
39+
* @returns {Array} - [loading, data, error, execute] - loading, data and error properties of the operation & a function to execute the operation again forcefully without any dependance change.
40+
*/
41+
export function useAsync(
42+
fn: () => Promise<any>,
43+
options?: Options,
44+
deps: Array<any> = []
45+
): Array<any> {
46+
const [state, dispatch] = useReducer(reducer, initialState);
47+
const { loading, data, error } = state;
48+
49+
const { successCallback, errorCallback } = options || {};
50+
const dependencyList = Array.isArray(deps) ? deps : [];
51+
52+
const execute = async (didCancel: boolean) => {
53+
try {
54+
if (!loading) dispatch({ type: 'loading', data: true });
55+
56+
const response = await fn();
57+
58+
if (!didCancel) {
59+
dispatch({ type: 'data', data: response });
60+
dispatch({ type: 'error', data: null });
61+
callFunction(successCallback, [response]);
62+
}
63+
} catch (err) {
64+
if (!didCancel) {
65+
dispatch({ type: 'error', data: err });
66+
callFunction(errorCallback, [err]);
67+
}
68+
} finally {
69+
if (!didCancel) {
70+
dispatch({ type: 'loading', data: false });
71+
}
72+
}
73+
};
74+
75+
useEffect(() => {
76+
let didCancel = false;
77+
78+
execute(didCancel);
79+
80+
return () => {
81+
didCancel = true;
82+
};
83+
}, dependencyList);
84+
85+
return [data, loading, error, execute];
86+
}

0 commit comments

Comments
 (0)