mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: isMounted & login query hooks
This commit is contained in:
parent
e741c2b913
commit
960bec4d5e
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
17
src/hooks/useIsMountedRef.ts
Normal file
17
src/hooks/useIsMountedRef.ts
Normal 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;
|
||||
68
src/hooks/useLoginQuery.ts
Normal file
68
src/hooks/useLoginQuery.ts
Normal 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;
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user