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 React from 'react';
|
||||||
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
||||||
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon, useToast } from '@chakra-ui/react';
|
import { FormControl, FormLabel, InputGroup, Input, InputLeftElement, Button, Icon } from '@chakra-ui/react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import useLoginQuery from 'hooks/useLoginQuery';
|
||||||
|
|
||||||
type AuthInputs = {
|
type AuthInputs = {
|
||||||
login: string;
|
login: string;
|
||||||
@ -13,35 +11,13 @@ type AuthInputs = {
|
|||||||
|
|
||||||
const AuthForm: React.FC = () => {
|
const AuthForm: React.FC = () => {
|
||||||
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
||||||
const { authStore } = useRootStore();
|
const { login, loading } = useLoginQuery();
|
||||||
const handleError = useErrorHandler();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<AuthInputs> = async data => {
|
const onSubmit: SubmitHandler<AuthInputs> = async data => {
|
||||||
try {
|
try {
|
||||||
await authStore.login(data.login, data.password);
|
await login(data.login, data.password);
|
||||||
toast({
|
|
||||||
title: `👋 Welcome back!`,
|
|
||||||
description: 'You are successfully logged in.',
|
|
||||||
isClosable: true,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response) {
|
console.error(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 {
|
} finally {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
@ -65,7 +41,7 @@ const AuthForm: React.FC = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
<Button
|
||||||
loadingText='Logging in...'
|
loadingText='Logging in...'
|
||||||
isLoading={authStore.loading}
|
isLoading={loading}
|
||||||
type='submit'
|
type='submit'
|
||||||
colorScheme='blue'
|
colorScheme='blue'
|
||||||
variant='outline'
|
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 NavList from '../NavList';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -26,7 +27,7 @@ const Nav: React.FC = () => {
|
|||||||
height='calc(100vh - 4rem)'
|
height='calc(100vh - 4rem)'
|
||||||
padding='5'
|
padding='5'
|
||||||
>
|
>
|
||||||
<NavList />
|
<NavList links={config.links} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/mod
|
|||||||
import { HamburgerIcon } from '@chakra-ui/icons';
|
import { HamburgerIcon } from '@chakra-ui/icons';
|
||||||
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
|
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
|
||||||
import NavList from '../NavList';
|
import NavList from '../NavList';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
const NavMobile: React.FC = () => {
|
const NavMobile: React.FC = () => {
|
||||||
const { userStore } = useRootStore();
|
const { userStore } = useRootStore();
|
||||||
@ -24,7 +25,7 @@ const NavMobile: React.FC = () => {
|
|||||||
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
|
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
|
||||||
<DrawerBody>
|
<DrawerBody>
|
||||||
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
|
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
|
||||||
<NavList onCloseDrawer={onClose} />
|
<NavList links={config.links} onCloseDrawer={onClose} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{userStore.me ? (
|
{userStore.me ? (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -1,34 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, useColorMode } from '@chakra-ui/react';
|
import { Box, useColorMode } from '@chakra-ui/react';
|
||||||
import type { LinkType } from 'interfaces';
|
import type { LinkType } from 'interfaces';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
export type NavLinkProps = {
|
type NavLinkPropsType = {
|
||||||
link: LinkType;
|
link: LinkType;
|
||||||
onCloseDrawer?: () => void;
|
onClickLink: (link: LinkType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the link component for navigation.
|
* Returns the link component for navigation.
|
||||||
*/
|
*/
|
||||||
const NavLink: React.FC<NavLinkProps> = ({ link, onCloseDrawer }) => {
|
const NavLink: React.FC<NavLinkPropsType> = ({ link, onClickLink }) => {
|
||||||
const { uiStore } = useRootStore();
|
const { uiStore } = useRootStore();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const onClick = () => onClickLink(link);
|
||||||
history.push(link.location);
|
|
||||||
|
|
||||||
/* Close mobile drawer on click link */
|
|
||||||
onCloseDrawer && onCloseDrawer();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as='a'
|
as='a'
|
||||||
onClick={handleClick}
|
onClick={onClick}
|
||||||
display='flex'
|
display='flex'
|
||||||
margin='3px 0'
|
margin='3px 0'
|
||||||
width='100%'
|
width='100%'
|
||||||
@ -41,7 +34,7 @@ const NavLink: React.FC<NavLinkProps> = ({ link, onCloseDrawer }) => {
|
|||||||
color={uiStore.location === link.location ? 'blue.400' : ''}
|
color={uiStore.location === link.location ? 'blue.400' : ''}
|
||||||
backgroundColor={uiStore.location === link.location ? `${colorMode === 'dark' ? 'gray.700' : 'gray.100'}` : ''}
|
backgroundColor={uiStore.location === link.location ? `${colorMode === 'dark' ? 'gray.700' : 'gray.100'}` : ''}
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
borderRadius='9999px'
|
borderRadius='3xl'
|
||||||
display='flex'
|
display='flex'
|
||||||
padding='10px 15px'
|
padding='10px 15px'
|
||||||
fontWeight='500'
|
fontWeight='500'
|
||||||
|
|||||||
@ -1,31 +1,37 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import config from 'config';
|
|
||||||
import NavLink from './NavLink';
|
import NavLink from './NavLink';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
// import { useRootStore } from 'hooks/useRootStore';
|
||||||
import { userHasRoles } from 'helpers';
|
// import { userHasRoles } from 'helpers';
|
||||||
import { UserRoleEnum } from 'enums';
|
// 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
|
* Render a navigation list of links
|
||||||
*/
|
*/
|
||||||
const NavList: React.FC<{ onCloseDrawer?: () => void }> = ({ onCloseDrawer }) => {
|
const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
|
||||||
const { userStore } = useRootStore();
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{config.links.map((link, i) => {
|
{links.map(link => {
|
||||||
if (link.public) {
|
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
|
||||||
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;
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
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 { useToast } from '@chakra-ui/react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useRootStore } from './useRootStore';
|
import { useRootStore } from './useRootStore';
|
||||||
import { IDataResult } from 'interfaces';
|
import { IDataResult } from 'interfaces';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import useIsMountedRef from './useIsMountedRef';
|
||||||
|
|
||||||
type StateType = {
|
type LogoutStateType = {
|
||||||
result: IDataResult | null;
|
result: IDataResult | null;
|
||||||
error: any;
|
error: any;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -14,34 +15,33 @@ type StateType = {
|
|||||||
|
|
||||||
const useLogoutQuery = () => {
|
const useLogoutQuery = () => {
|
||||||
const { authStore } = useRootStore();
|
const { authStore } = useRootStore();
|
||||||
const [state, setState] = React.useState<StateType>({ result: null, error: null, loading: false, fetched: false });
|
const [state, setState] = useState<LogoutStateType>({ result: null, error: null, loading: false, fetched: false });
|
||||||
const isMounted = React.useRef(true);
|
const isMountedRef = useIsMountedRef();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
const request = React.useCallback(async () => {
|
|
||||||
|
const logout = async () => {
|
||||||
setState(s => ({ ...s, loading: true }));
|
setState(s => ({ ...s, loading: true }));
|
||||||
try {
|
try {
|
||||||
const data = await authStore.logout();
|
const data = await authStore.logout();
|
||||||
history.push('/auth');
|
history.push('/auth');
|
||||||
|
|
||||||
toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
|
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) {
|
} catch (error) {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setState(s => ({ ...s, error }));
|
||||||
|
}
|
||||||
handleError(error);
|
handleError(error);
|
||||||
setState(s => ({ ...s, error }));
|
|
||||||
} finally {
|
} 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 };
|
return { ...state, logout };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { observer } from 'mobx-react';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box } from '@chakra-ui/react';
|
import Helmet from 'react-helmet';
|
||||||
import { MdSchool } from 'react-icons/md';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import { IPageProps } from 'interfaces';
|
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 AuthForm from 'components/Auth/AuthForm';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth page
|
* Auth page
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
import { IDataResult, ILoginResult } from 'interfaces';
|
import { IDataResult, ILoginResult } from 'interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,8 +20,6 @@ export default class AuthStore {
|
|||||||
|
|
||||||
public isLoggedIn: boolean = !!Cookies.get('logged_in');
|
public isLoggedIn: boolean = !!Cookies.get('logged_in');
|
||||||
|
|
||||||
public loading: boolean = false;
|
|
||||||
|
|
||||||
constructor(root: RootStore, api: AxiosInstance) {
|
constructor(root: RootStore, api: AxiosInstance) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.authService = new AuthService(api);
|
this.authService = new AuthService(api);
|
||||||
@ -32,13 +30,8 @@ export default class AuthStore {
|
|||||||
this.isLoggedIn = value;
|
this.isLoggedIn = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLoading(value: boolean) {
|
|
||||||
this.loading = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async login(login: string, password: string): Promise<ILoginResult> {
|
public async login(login: string, password: string): Promise<ILoginResult> {
|
||||||
try {
|
try {
|
||||||
this.setLoading(true);
|
|
||||||
const result = await this.authService.requestLogin(login, password);
|
const result = await this.authService.requestLogin(login, password);
|
||||||
|
|
||||||
this.setIsLoggedIn(true);
|
this.setIsLoggedIn(true);
|
||||||
@ -47,14 +40,11 @@ export default class AuthStore {
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
this.setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(): Promise<IDataResult> {
|
public async logout(): Promise<IDataResult> {
|
||||||
try {
|
try {
|
||||||
this.setLoading(true);
|
|
||||||
const result = await this.authService.requestLogout();
|
const result = await this.authService.requestLogout();
|
||||||
|
|
||||||
this.setIsLoggedIn(false);
|
this.setIsLoggedIn(false);
|
||||||
@ -63,8 +53,6 @@ export default class AuthStore {
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
this.setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user