Skip to content

Commit

Permalink
chore(react-core): update useDataState - add hasError property, resol…
Browse files Browse the repository at this point in the history
…veMaybeAsync util (#5400)
  • Loading branch information
calebpollman authored Jul 15, 2024
1 parent f9d1ddd commit 4fed60c
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 21 deletions.
56 changes: 54 additions & 2 deletions packages/react-core/src/hooks/__tests__/useDataState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('useDataState', () => {
expect(action).not.toHaveBeenCalled();

expect(initState.data).toBe(initData);
expect(initState.hasError).toBe(false);
expect(initState.isLoading).toBe(false);
expect(initState.message).toBeUndefined();

Expand All @@ -41,6 +42,7 @@ describe('useDataState', () => {
expect(action).toHaveBeenCalledWith(initData, nextData);

expect(loadingState.data).toBe(initData);
expect(loadingState.hasError).toBe(false);
expect(loadingState.isLoading).toBe(true);
expect(loadingState.message).toBeUndefined();

Expand All @@ -53,11 +55,61 @@ describe('useDataState', () => {
expect(action).toHaveBeenCalledWith(initData, nextData);

expect(nextState.data).toBe(nextData);
expect(nextState.hasError).toBe(false);
expect(nextState.isLoading).toBe(false);
expect(nextState.message).toBeUndefined();
}
);

it.todo('only returns the values of the last call to handleAction');
it.todo('handles exceptions thrown from provided action');
it('handles an error and resets error state on the next call to handleAction', async () => {
const errorMessage = 'Unhappy!';
const unhappyAction = jest.fn((_, isUnhappy: boolean) =>
isUnhappy ? Promise.reject(new Error(errorMessage)) : Promise.resolve()
);

const { result, waitForNextUpdate } = renderHook(() =>
useDataState(unhappyAction, undefined)
);

const [initialState, handleAction] = result.current;

expect(unhappyAction).not.toHaveBeenCalled();

expect(initialState.hasError).toBe(false);
expect(initialState.isLoading).toBe(false);
expect(initialState.message).toBeUndefined();

act(() => {
handleAction(true);
});

const [loadingState] = result.current;

expect(loadingState.hasError).toBe(false);
expect(loadingState.isLoading).toBe(true);
expect(loadingState.message).toBeUndefined();

await waitForNextUpdate();

const [errorState] = result.current;

expect(errorState.hasError).toBe(true);
expect(errorState.isLoading).toBe(false);
expect(errorState.message).toBe(errorMessage);

act(() => {
handleAction(false);
});

const [nextLoadingState] = result.current;

expect(nextLoadingState.hasError).toBe(false);
expect(nextLoadingState.isLoading).toBe(true);
expect(nextLoadingState.message).toBeUndefined();

// cleanup
await waitForNextUpdate();
});

it.todo('only returns the value of the last call to handleAction');
});
3 changes: 2 additions & 1 deletion packages/react-core/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as useDataState } from './useDataState';
export { default as useDataState, DataState } from './useDataState';

export {
default as useDeprecationWarning,
UseDeprecationWarning,
Expand Down
44 changes: 26 additions & 18 deletions packages/react-core/src/hooks/useDataState.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,50 @@
import React from 'react';

interface ActionState<T> {
export interface DataState<T> {
data: T;
hasError: boolean;
isLoading: boolean;
message: string | undefined;
}

const getActionState = <T>(data: T): ActionState<T> => ({
data,
isLoading: false,
message: undefined,
});
// default state
const INITIAL_STATE = { hasError: false, isLoading: false, message: undefined };
const LOADING_STATE = { hasError: false, isLoading: true, message: undefined };
const ERROR_STATE = { hasError: true, isLoading: false };

const resolveMaybeAsync = async <T>(
value: T | Promise<T>
): Promise<Awaited<T>> => {
const awaited = await value;
return awaited;
};

export default function useDataState<T, K>(
action: (prevData: Awaited<T>, ...input: K[]) => T | Promise<T>,
initialData: Awaited<T>
): [state: ActionState<Awaited<T>>, handleAction: (...input: K[]) => void] {
const [actionState, setActionState] = React.useState<ActionState<Awaited<T>>>(
() => getActionState(initialData)
);
action: (prevData: T, ...input: K[]) => T | Promise<T>,
initialData: T
): [state: DataState<T>, handleAction: (...input: K[]) => void] {
const [dataState, setDataState] = React.useState<DataState<T>>(() => ({
...INITIAL_STATE,
data: initialData,
}));

const prevData = React.useRef(initialData);

const handleAction: (...input: K[]) => void = React.useCallback(
(...input) => {
setActionState((prev) => ({ ...prev, isLoading: true }));
setDataState(({ data }) => ({ ...LOADING_STATE, data }));

Promise.resolve(action(prevData.current, ...input))
.then((data) => {
resolveMaybeAsync(action(prevData.current, ...input))
.then((data: T) => {
prevData.current = data;
setActionState(getActionState(data));
setDataState({ ...INITIAL_STATE, data });
})
.catch(({ message }: Error) => {
setActionState((prev) => ({ ...prev, isLoading: false, message }));
setDataState(({ data }) => ({ ...ERROR_STATE, data, message }));
});
},
[action]
);

return [actionState, handleAction];
return [dataState, handleAction];
}

0 comments on commit 4fed60c

Please sign in to comment.