feat: added useAsync hook

This commit is contained in:
Sergey Yarkov 2021-11-20 21:45:32 +03:00
parent 80ddcead4f
commit bf55565eed
6 changed files with 55 additions and 35 deletions

41
src/hooks/useAsync.ts Normal file
View File

@ -0,0 +1,41 @@
import { useCallback, useEffect, useState } from 'react';
import useIsMountedRef from './useIsMountedRef';
type StateType<DataResultT> = {
status: 'idle' | 'pending' | 'success' | 'error';
error: any;
data: DataResultT | null;
isLoading: boolean;
};
const useAsync = <T, P extends unknown[]>(callback: (...params: P) => Promise<T>) => {
const [state, setState] = useState<StateType<T>>({ status: 'idle', error: null, data: null, isLoading: false });
const isMountedRef = useIsMountedRef();
const execute = useCallback(
async (...params: P) => {
try {
setState(s => ({ ...s, isLoading: true, status: 'pending', data: null, error: null }));
const res = await callback(...params);
if (isMountedRef.current) {
setState(s => ({ ...s, status: 'success', data: res }));
}
return res;
} catch (error: any) {
if (isMountedRef.current) {
setState(s => ({ ...s, status: 'error', error }));
}
throw error;
} finally {
if (isMountedRef.current) {
setState(s => ({ ...s, isLoading: false }));
}
}
},
[callback]
);
return { execute, ...state };
};
export default useAsync;

View File

@ -1,27 +1,16 @@
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { IApiRespose, IToken } from '@educt/interfaces'; import { IApiRespose, IToken } from '@educt/interfaces';
import { useState } from 'react';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import useIsMountedRef from './useIsMountedRef'; import useAsync from './useAsync';
import { useRootStore } from './useRootStore'; import { useRootStore } from './useRootStore';
type LoginStateType = {
result: IApiRespose<IToken> | null;
error: any;
loading: boolean;
fetched: boolean;
};
const useLoginQuery = () => { const useLoginQuery = () => {
const isMountedRef = useIsMountedRef();
const { authStore } = useRootStore(); const { authStore } = useRootStore();
const [state, setState] = useState<LoginStateType>({ result: null, error: null, loading: false, fetched: false });
const toast = useToast(); const toast = useToast();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const login = async (login: string, password: string) => { const login = async (login: string, password: string) => {
try { try {
setState(s => ({ ...s, loading: true }));
const result = await authStore.login(login, password); const result = await authStore.login(login, password);
toast({ toast({
title: `👋 Welcome back!`, title: `👋 Welcome back!`,
@ -29,14 +18,8 @@ const useLoginQuery = () => {
isClosable: true, isClosable: true,
status: 'success', status: 'success',
}); });
return result;
if (isMountedRef.current) {
setState(s => ({ ...s, result }));
}
} catch (error: any) { } catch (error: any) {
if (isMountedRef.current) {
setState(s => ({ ...s, error }));
}
if (error.response) { if (error.response) {
/** /**
* Handle response error * Handle response error
@ -44,26 +27,23 @@ const useLoginQuery = () => {
switch (error.response.status) { switch (error.response.status) {
case 404: case 404:
toast({ title: 'User not found in a system.', status: 'error', duration: 2000 }); toast({ title: 'User not found in a system.', status: 'error', duration: 2000 });
break; throw error;
case 401: case 401:
toast({ title: 'Invalid password.', status: 'error', duration: 2000 }); toast({ title: 'Invalid password.', status: 'error', duration: 2000 });
break; throw error;
default: default:
handleError(error); handleError(error);
break; break;
} }
} else { } else {
console.error(error);
handleError(error); handleError(error);
} }
} finally {
if (isMountedRef.current) {
setState(s => ({ ...s, loading: false, fetched: true }));
}
} }
}; };
return { ...state, login }; const { execute, ...state } = useAsync<IApiRespose<IToken> | undefined, Parameters<typeof login>>(login);
return { login: execute, ...state };
}; };
export default useLoginQuery; export default useLoginQuery;

View File

@ -27,6 +27,7 @@ export interface ICourse {
category: ICategory; category: ICategory;
lessons: ILesson[]; lessons: ILesson[];
students: Array<Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email'>>; students: Array<Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email'>>;
color: ColorType | null;
students_count: string; students_count: string;
likes_count: string; likes_count: string;
lessons_count: string; lessons_count: string;

View File

@ -44,14 +44,12 @@ const AuthForm: React.FC = () => {
} = useForm<AuthInputType>({ } = useForm<AuthInputType>({
resolver: yupResolver(LoginSchema), resolver: yupResolver(LoginSchema),
}); });
const { login, loading } = useLoginQuery(); const { login, isLoading } = useLoginQuery();
/** /**
* Login handler * Login handler
*/ */
const onSubmit: SubmitHandler<AuthInputType> = data => { const onSubmit: SubmitHandler<AuthInputType> = data => login(data.login, data.password).finally(() => reset());
login(data.login, data.password).finally(() => reset());
};
return ( return (
<Box flexBasis='500px' borderWidth='1px' borderRadius='lg' p='8'> <Box flexBasis='500px' borderWidth='1px' borderRadius='lg' p='8'>
@ -87,7 +85,7 @@ const AuthForm: React.FC = () => {
<Button <Button
loadingText='Logging in...' loadingText='Logging in...'
isLoading={loading} isLoading={isLoading}
type='submit' type='submit'
colorScheme='blue' colorScheme='blue'
variant='outline' variant='outline'

View File

@ -23,7 +23,7 @@ import CourseStatusBadge from '@educt/components/CourseStatusBadge';
* Hooks * Hooks
*/ */
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useColorMode } from '@chakra-ui/color-mode'; import { useColorMode, useColorModeValue } from '@chakra-ui/color-mode';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
export type CourseItemPropsType = { export type CourseItemPropsType = {
@ -108,7 +108,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
<Box <Box
position='relative' position='relative'
minHeight='200px' minHeight='200px'
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.200'} backgroundColor={course.color?.hex || useColorModeValue('gray.200', 'gray.700')}
borderTopLeftRadius='lg' borderTopLeftRadius='lg'
borderTopRightRadius='lg' borderTopRightRadius='lg'
p='10px' p='10px'

View File

@ -69,7 +69,7 @@ const LessonItem: React.FC<LessonItemPropsType> = ({ lesson }) => {
<Icon as={MdTimer} /> <Icon as={MdTimer} />
</Text> </Text>
<Text as='small' verticalAlign='middle'> <Text as='small' verticalAlign='middle'>
~{moment(lesson.duration, 'HH:mm:ss').minutes()}min ~{moment.duration(lesson.duration, 'minutes').humanize()}
</Text> </Text>
</Text> </Text>
<Text as='span' color='gray.500'> <Text as='span' color='gray.500'>