Skip to content

Commit

Permalink
feat(useAsyncActionHandler): add useAsyncActionHandler hook (#1095)
Browse files Browse the repository at this point in the history
Co-authored-by: kseniyakuzina <[email protected]>
  • Loading branch information
kseniya57 and kseniyakuzina authored Nov 14, 2023
1 parent 2a3eccc commit 66a25b3
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './useActionHandlers';
export * from './useAsyncActionHandler';
export * from './useBodyScrollLock';
export * from './useFileInput';
export * from './useFocusWithin';
Expand Down
89 changes: 89 additions & 0 deletions src/hooks/useAsyncActionHandler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!--GITHUB_BLOCK-->

# useAsyncActionHandler

<!--/GITHUB_BLOCK-->

```tsx
import {useAsyncActionHandler} from '@gravity-ui/uikit';
```

The `useAsyncActionHandler` hook wraps an asynchronous action handler to add a loading state to it.
Starts the loading process before executing the passed action and terminates the process after executing the action.
Returns the loading state and the wrapped action handler

## Properties

| Name | Description | Type | Default |
| :------ | :------------- | :---------------------------------------------: | :-----: |
| handler | action handler | `(...args: unknown[]) => PromiseLike<unknown>;` | |

## Result

```ts
{
isLoading: boolean;
handler: typeof handler;
}
```

Usage:

```tsx
const action = useCallback(() => Promise.resolve(), []);

const {isLoading, handler: handleAction} = useAsyncActionHandler({handler: action});
```

### Examples

Button with automatic loading during click handler execution

```tsx
type ProgressButtonProps = {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => PromiseLike<unknown>;
} & Exclude<ButtonProps, 'onClick'>;

export const ProgressButton = (props: ProgressButtonProps) => {
const {onClick, loading: providedIsLoading} = props;

const {isLoading, handler: handleClick} = useAsyncActionHandler({handler: onClick});

return <Button {...props} onClick={handleClick} loading={isLoading || providedIsLoading} />;
};

export const LoadableList = () => {
const [items, setItems] = useState([]);

const handleLoadItems = useCallback(async () => {
try {
const loadItems = () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(
Array.from(
{length: 10},
(_item, index) => `Item ${Math.random() * 100 * (index + 1)}`,
),
);
}, 1000);
});

setItems(await loadItems());
} catch (error) {
console.error(error);
}
}, []);

return (
<>
<ProgressButton onClick={handleLoadItems}>Load items</ProgressButton>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
);
};
```
16 changes: 16 additions & 0 deletions src/hooks/useAsyncActionHandler/__stories__/ProgressButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import {Button, ButtonProps} from '../../../components';
import {useAsyncActionHandler} from '../useAsyncActionHandler';

export type ProgressButtonProps = {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => PromiseLike<unknown>;
} & Exclude<ButtonProps, 'onClick'>;

export const ProgressButton = (props: ProgressButtonProps) => {
const {onClick, loading: providedIsLoading} = props;

const {isLoading, handler: handleClick} = useAsyncActionHandler({handler: onClick});

return <Button {...props} onClick={handleClick} loading={isLoading || providedIsLoading} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {block} from '../../../components/utils/cn';

export const cnUseAsyncActionHandlerDemo = block('async-action-handler-demo');
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import '../../../components/variables';

$block: '.#{$ns}async-action-handler-demo';

#{$block} {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 16px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

import {ProgressButton} from './ProgressButton';
import {cnUseAsyncActionHandlerDemo} from './UseAsyncActionHandlerDemo.classname';

import './UseAsyncActionHandlerDemo.scss';

export const UseAsyncActionHandlerDemo = () => {
const [items, setItems] = React.useState<string[]>([]);

const handleLoadItems = React.useCallback(async () => {
const loadItems = () =>
new Promise<string[]>((resolve) => {
setTimeout(() => {
resolve(
Array.from(
{length: 10},
(_item, index) =>
`Item ${Math.round(Math.random() * 100 * (index + 1))}`,
),
);
}, 1000);
});

try {
setItems(await loadItems());
} catch (error) {
console.error(error);
}
}, []);

return (
<div className={cnUseAsyncActionHandlerDemo()}>
<ProgressButton onClick={handleLoadItems}>Load items</ProgressButton>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

import type {Meta, StoryFn} from '@storybook/react';

import {UseAsyncActionHandlerDemo} from './UseAsyncActionHandlerDemo';

export default {
title: 'Hooks/useAsyncActionHandler',
} as Meta;

export const Showcase: StoryFn = () => <UseAsyncActionHandlerDemo />;
1 change: 1 addition & 0 deletions src/hooks/useAsyncActionHandler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useAsyncActionHandler';
36 changes: 36 additions & 0 deletions src/hooks/useAsyncActionHandler/useAsyncActionHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';

type AnyAsyncAction = (...args: any[]) => PromiseLike<any>;

export interface UseAsyncActionHandlerProps<Action extends AnyAsyncAction> {
handler: Action;
}

export interface UseAsyncActionHandlerResult<Action extends AnyAsyncAction> {
isLoading: boolean;
handler: (...args: Parameters<Action>) => Promise<Awaited<ReturnType<Action>>>;
}

export function useAsyncActionHandler<Action extends AnyAsyncAction>({
handler,
}: UseAsyncActionHandlerProps<Action>): UseAsyncActionHandlerResult<Action> {
const [isLoading, setLoading] = React.useState(false);

const handleAction = React.useCallback<UseAsyncActionHandlerResult<Action>['handler']>(
async (...args) => {
setLoading(true);

try {
return await handler(...args);
} finally {
setLoading(false);
}
},
[handler],
);

return {
isLoading,
handler: handleAction,
};
}

0 comments on commit 66a25b3

Please sign in to comment.