feat: isMounted & login query hooks

This commit is contained in:
Sergey Yarkov 2021-08-28 23:07:06 +03:00
parent e741c2b913
commit 960bec4d5e
10 changed files with 151 additions and 101 deletions

View File

@ -1,10 +1,8 @@
import React from 'react';
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
import { useRootStore } from 'hooks/useRootStore';
import { observer } from 'mobx-react';
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon } from '@chakra-ui/react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useErrorHandler } from 'react-error-boundary';
import useLoginQuery from 'hooks/useLoginQuery';
type AuthInputs = {
login: string;
@ -13,35 +11,13 @@ type AuthInputs = {
const AuthForm: React.FC = () => {
const { register, reset, handleSubmit } = useForm<AuthInputs>();
const { authStore } = useRootStore();
const handleError = useErrorHandler();
const toast = useToast();
const { login, loading } = useLoginQuery();
const onSubmit: SubmitHandler<AuthInputs> = async data => {
try {
await authStore.login(data.login, data.password);
toast({
title: `👋 Welcome back!`,
description: 'You are successfully logged in.',
isClosable: true,
status: 'success',
});
await login(data.login, data.password);
} catch (error) {
if (error.response) {
switch (error.response.status) {
case 404:
toast({ title: 'User not found in a system.', status: 'error', duration: 2000 });
break;
case 401:
toast({ title: 'Invalid password.', status: 'error', duration: 2000 });
break;
default:
handleError(error);
break;
}
} else {
handleError(error);
}
console.error(error);
} finally {
reset();
}
@ -65,7 +41,7 @@ const AuthForm: React.FC = () => {
</FormControl>
<Button
loadingText='Logging in...'
isLoading={authStore.loading}
isLoading={loading}
type='submit'
colorScheme='blue'
variant='outline'
@ -78,4 +54,4 @@ const AuthForm: React.FC = () => {
);
};
export default observer(AuthForm);
export default AuthForm;

View File

@ -3,6 +3,7 @@ import { Flex, Box } from '@chakra-ui/react';
import NavList from '../NavList';
import { useRootStore } from 'hooks/useRootStore';
import { observer } from 'mobx-react';
import config from 'config';
/**
*
@ -26,7 +27,7 @@ const Nav: React.FC = () => {
height='calc(100vh - 4rem)'
padding='5'
>
<NavList />
<NavList links={config.links} />
</Flex>
</Box>
</Box>

View File

@ -10,6 +10,7 @@ import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/mod
import { HamburgerIcon } from '@chakra-ui/icons';
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
import NavList from '../NavList';
import config from 'config';
const NavMobile: React.FC = () => {
const { userStore } = useRootStore();
@ -24,7 +25,7 @@ const NavMobile: React.FC = () => {
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
<DrawerBody>
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
<NavList onCloseDrawer={onClose} />
<NavList links={config.links} onCloseDrawer={onClose} />
</Flex>
{userStore.me ? (
<Box

View File

@ -1,34 +1,27 @@
import React from 'react';
import { Box, useColorMode } from '@chakra-ui/react';
import type { LinkType } from 'interfaces';
import { useHistory } from 'react-router-dom';
import { observer } from 'mobx-react';
import { useRootStore } from 'hooks/useRootStore';
export type NavLinkProps = {
type NavLinkPropsType = {
link: LinkType;
onCloseDrawer?: () => void;
onClickLink: (link: LinkType) => void;
};
/**
* Returns the link component for navigation.
*/
const NavLink: React.FC<NavLinkProps> = ({ link, onCloseDrawer }) => {
const NavLink: React.FC<NavLinkPropsType> = ({ link, onClickLink }) => {
const { uiStore } = useRootStore();
const { colorMode } = useColorMode();
const history = useHistory();
const handleClick = () => {
history.push(link.location);
/* Close mobile drawer on click link */
onCloseDrawer && onCloseDrawer();
};
const onClick = () => onClickLink(link);
return (
<Box
as='a'
onClick={handleClick}
onClick={onClick}
display='flex'
margin='3px 0'
width='100%'
@ -41,7 +34,7 @@ const NavLink: React.FC<NavLinkProps> = ({ link, onCloseDrawer }) => {
color={uiStore.location === link.location ? 'blue.400' : ''}
backgroundColor={uiStore.location === link.location ? `${colorMode === 'dark' ? 'gray.700' : 'gray.100'}` : ''}
alignItems='center'
borderRadius='9999px'
borderRadius='3xl'
display='flex'
padding='10px 15px'
fontWeight='500'

View File

@ -1,31 +1,37 @@
import React from 'react';
import React, { useCallback } from 'react';
import { observer } from 'mobx-react';
import config from 'config';
import NavLink from './NavLink';
import { useRootStore } from 'hooks/useRootStore';
import { userHasRoles } from 'helpers';
import { UserRoleEnum } from 'enums';
// import { useRootStore } from 'hooks/useRootStore';
// import { userHasRoles } from 'helpers';
// import { UserRoleEnum } from 'enums';
import { useHistory } from 'react-router-dom';
import { LinkType } from 'interfaces';
type NavListPropsType = {
links: LinkType[];
onCloseDrawer?: () => void;
};
/**
* Render a navigation list of links
*/
const NavList: React.FC<{ onCloseDrawer?: () => void }> = ({ onCloseDrawer }) => {
const { userStore } = useRootStore();
const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
// const { userStore } = useRootStore();
const history = useHistory();
const onClickLink = useCallback(
(link: LinkType) => {
history.push(link.location);
/* Close mobile drawer on click link */
onCloseDrawer && onCloseDrawer();
},
[history, onCloseDrawer]
);
return (
<>
{config.links.map((link, i) => {
if (link.public) {
return <NavLink link={link} key={i} onCloseDrawer={onCloseDrawer} />;
} else {
/**
* Render not public link
*/
if (userStore.me && userHasRoles(userStore.me.roles, [UserRoleEnum.ADMIN])) {
return <NavLink link={link} key={i} onCloseDrawer={onCloseDrawer} />;
}
return null;
}
{links.map(link => {
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
})}
</>
);

View File

@ -0,0 +1,17 @@
import { useRef, useEffect } from 'react';
const useIsMountedRef = () => {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef;
};
export default useIsMountedRef;

View File

@ -0,0 +1,68 @@
import { useToast } from '@chakra-ui/react';
import { ILoginResult } from 'interfaces';
import { useState } from 'react';
import { useErrorHandler } from 'react-error-boundary';
import useIsMountedRef from './useIsMountedRef';
import { useRootStore } from './useRootStore';
type LoginStateType = {
result: ILoginResult | null;
error: any;
loading: boolean;
fetched: boolean;
};
const useLoginQuery = () => {
const isMountedRef = useIsMountedRef();
const { authStore } = useRootStore();
const [state, setState] = useState<LoginStateType>({ result: null, error: null, loading: false, fetched: false });
const toast = useToast();
const handleError = useErrorHandler();
const login = async (login: string, password: string) => {
try {
setState(s => ({ ...s, loading: true }));
const result = await authStore.login(login, password);
toast({
title: `👋 Welcome back!`,
description: 'You are successfully logged in.',
isClosable: true,
status: 'success',
});
if (isMountedRef.current) {
setState(s => ({ ...s, result }));
}
} catch (error) {
if (isMountedRef.current) {
setState(s => ({ ...s, error }));
}
if (error.response) {
/**
* Handle response error
*/
switch (error.response.status) {
case 404:
toast({ title: 'User not found in a system.', status: 'error', duration: 2000 });
break;
case 401:
toast({ title: 'Invalid password.', status: 'error', duration: 2000 });
break;
default:
handleError(error);
break;
}
} else {
handleError(error);
}
} finally {
if (isMountedRef.current) {
setState(s => ({ ...s, loading: false, fetched: true }));
}
}
};
return { ...state, login };
};
export default useLoginQuery;

View File

@ -1,11 +1,12 @@
import React from 'react';
import { useState } from 'react';
import { useToast } from '@chakra-ui/react';
import { useHistory } from 'react-router-dom';
import { useRootStore } from './useRootStore';
import { IDataResult } from 'interfaces';
import { useErrorHandler } from 'react-error-boundary';
import useIsMountedRef from './useIsMountedRef';
type StateType = {
type LogoutStateType = {
result: IDataResult | null;
error: any;
loading: boolean;
@ -14,34 +15,33 @@ type StateType = {
const useLogoutQuery = () => {
const { authStore } = useRootStore();
const [state, setState] = React.useState<StateType>({ result: null, error: null, loading: false, fetched: false });
const isMounted = React.useRef(true);
const [state, setState] = useState<LogoutStateType>({ result: null, error: null, loading: false, fetched: false });
const isMountedRef = useIsMountedRef();
const history = useHistory();
const toast = useToast();
const handleError = useErrorHandler();
const request = React.useCallback(async () => {
const logout = async () => {
setState(s => ({ ...s, loading: true }));
try {
const data = await authStore.logout();
history.push('/auth');
toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
if (isMounted.current) setState(s => ({ ...s, result: data }));
if (isMountedRef.current) {
setState(s => ({ ...s, result: data }));
}
} catch (error) {
if (isMountedRef.current) {
setState(s => ({ ...s, error }));
}
handleError(error);
setState(s => ({ ...s, error }));
} finally {
if (isMounted.current) setState(s => ({ ...s, loading: false, fetched: true }));
if (isMountedRef.current) {
setState(s => ({ ...s, loading: false, fetched: true }));
}
}
}, [authStore, history, toast, handleError]);
const logout = () => request();
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
};
return { ...state, logout };
};

View File

@ -1,13 +1,13 @@
import { observer } from 'mobx-react';
import Helmet from 'react-helmet';
import React from 'react';
import { Flex, Box } from '@chakra-ui/react';
import { MdSchool } from 'react-icons/md';
import Helmet from 'react-helmet';
import { Redirect } from 'react-router-dom';
import { observer } from 'mobx-react';
import { IPageProps } from 'interfaces';
import { Flex, Box } from '@chakra-ui/react';
import { useRootStore } from 'hooks/useRootStore';
import { MdSchool } from 'react-icons/md';
import AuthForm from 'components/Auth/AuthForm';
import { useRootStore } from 'hooks/useRootStore';
import { Redirect } from 'react-router-dom';
/**
* Auth page

View File

@ -1,6 +1,6 @@
import { makeAutoObservable } from 'mobx';
import Cookies from 'js-cookie';
import { AxiosInstance } from 'axios';
import Cookies from 'js-cookie';
import { IDataResult, ILoginResult } from 'interfaces';
/**
@ -20,8 +20,6 @@ export default class AuthStore {
public isLoggedIn: boolean = !!Cookies.get('logged_in');
public loading: boolean = false;
constructor(root: RootStore, api: AxiosInstance) {
this.root = root;
this.authService = new AuthService(api);
@ -32,13 +30,8 @@ export default class AuthStore {
this.isLoggedIn = value;
}
public setLoading(value: boolean) {
this.loading = value;
}
public async login(login: string, password: string): Promise<ILoginResult> {
try {
this.setLoading(true);
const result = await this.authService.requestLogin(login, password);
this.setIsLoggedIn(true);
@ -47,14 +40,11 @@ export default class AuthStore {
return result;
} catch (error) {
throw error;
} finally {
this.setLoading(false);
}
}
public async logout(): Promise<IDataResult> {
try {
this.setLoading(true);
const result = await this.authService.requestLogout();
this.setIsLoggedIn(false);
@ -63,8 +53,6 @@ export default class AuthStore {
return result;
} catch (error) {
throw error;
} finally {
this.setLoading(false);
}
}
}