Article Directory
-
- 1. Project launch: project initialization and configuration
- 2. React and Hook application: implement the project list
- 3. TS Application: JS God Assist - Strong Type
- 4. JWT, user authentication and asynchronous request
- 5. CSS is actually very simple - add styles with CSS-in-JS
- 6. User experience optimization - loading and error state handling
- 7. Hook, routing, and URL state management
- 8. User selector and item editing function
- 9. In-depth React state management and Redux mechanism
Source of learning content: React + React Hook + TS Best Practice - MOOC
Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):
item | Version |
---|---|
react & react-dom | ^18.2.0 |
react-router & react-router-dom | ^6.11.2 |
antd | ^4.24.8 |
@commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
eslint-config-prettier | ^8.6.0 |
husky | ^8.0.3 |
lint-staged | ^13.1.2 |
prettier | 2.8.4 |
json-server | 0.17.2 |
craco-less | ^2.0.0 |
@craco/craco | ^7.1.0 |
qs | ^6.11.0 |
dayjs | ^1.11.7 |
react-helmet | ^6.1.0 |
@types/react-helmet | ^6.1.6 |
react-query | ^6.1.0 |
@welldone-software/why-did-you-render | ^7.0.1 |
@emotion/react & @emotion/styled | ^11.10.6 |
The specific configuration, operation and content will be different, and the "pit" will also be different. . .
1. Project launch: project initialization and configuration
2. React and Hook application: implement the project list
3. TS Application: JS God Assist - Strong Type
4. JWT, user authentication and asynchronous request
5. CSS is actually very simple - add styles with CSS-in-JS
6. User experience optimization - loading and error state handling
7. Hook, routing, and URL state management
8. User selector and item editing function
9. In-depth React state management and Redux mechanism
1. useCallback application to optimize asynchronous requests
In the current project, use to useAsync
make asynchronous requests, but there is a hidden one bug
. If a request is initiated on the page, the request will take a long time of 3s (you can use the development console to set the minimum request time to preset the scene), during this time period , log out, and an error will be reported at this time:
Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
setXXX
The reason is that although the asynchronous function is still executing after logging out and the component is destroyed, the corresponding destroyed component cannot be found when it completes the next operation or updates the component.
Let's solve this problem next.
EDIT src\utils\index.ts
:
...
/**
* 返回组件的挂载状态,如果还没有挂载或者已经卸载,返回 false; 反之,返回 true;
*/
export const useMountedRef = () => {
const mountedRef = useRef(false)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return mountedRef
}
Apply on src\utils\use-async.ts
:
...
import {
useMountedRef } from "utils";
...
export const useAsync = <D>(...) => {
...
const mountedRef = useMountedRef()
...
const run = (...) => {
...
return promise
.then((data) => {
if(mountedRef.current)
setData(data);
return data;
})
.catch((error) => {
...});
};
...
};
There is still a remaining problem. useEffect
If the variables used in are not added in the dependent array , an error will be reported, and adding them will cause an infinite loop, so it was eslint-disable-next-line
solved with before
// eslint-disable-next-line react-hooks/exhaustive-deps
Now to change the solution, use useMemo
can of course be solved, here it is recommended to use a special version useMemo
, useCallback
:
Revisesrc\utils\use-async.ts
import {
useCallback, useState } from "react";
...
export const useAsync = <D>(...) => {
...
const setData = useCallback((data: D) =>
setState({
data,
stat: "success",
error: null,
}), [])
const setError = useCallback((error: Error) =>
setState({
error,
stat: "error",
data: null,
}), [])
// run 来触发异步请求
const run = useCallback((...) => {
...
}, [config.throwOnError, mountedRef, setData, state, setError],
)
...
};
You can follow the prompts to configure dependencies:
React Hook useCallback has missing dependencies: 'config.throwOnError', 'mountedRef', 'setData', and 'state'. Either include them or remove the dependency array. You can also do a functional update 'setState(s => ...)' if you only need 'state' in the 'setState' call.e
Even so, it is inevitable useCallback
to change the behavior of the dependent value in , for example, XXX
corresponding to the dependent value setXXX
, you need to use setXXX
the function usage of (this can also save a dependency):
Continue to modifysrc\utils\use-async.ts
...
export const useAsync = <D>(...) => {
...
const run = useCallback((...) => {
...
setState(prevState => ({
...prevState, stat: "loading" }));
...
}, [config.throwOnError, mountedRef, setData, setError],
)
...
};
Revisesrc\utils\project.ts
...
import {
useCallback, useEffect } from "react";
...
export const useProjects = (...) => {
...
const fetchProject = useCallback(() =>
client("projects", {
data: cleanObject(param || {
})
}), [client, param])
useEffect(() => {
run(fetchProject(), {
rerun: fetchProject });
}, [param, fetchProject, run]);
...
};
...
Revisesrc\utils\http.ts
...
import {
useCallback } from "react";
...
export const useHttp = () => {
...
return useCallback((...[funcPath, customConfig]: Parameters<typeof http>) =>
http(funcPath, {
...customConfig, token: user?.token }), [user?.token]);
};
Summary: Non-state types need to be used as dependencies useMemo
or useCallback
packaged (dependency refinement + old and new associations), which are common in Custom Hook
the return of function type data in
2. State promotion, composite components and inversion of control
Next, customize a project editing modal box (edit + new project), PageHeader
hover
and then open it (new), and ProjectList
open the modal box in it (new), and List
each line in it can also open the modal box (edit)
src\components\lib.tsx
Added in padding
as 0
of Button
:
...
export const ButtonNoPadding = styled(Button)`
padding: 0;
`
New src\screens\ProjectList\components\ProjectModal.tsx
(modal box):
import {
Button, Drawer } from "antd"
export const ProjectModal = ({
isOpen, onClose}: {
isOpen: boolean, onClose: () => void }) => {
return <Drawer onClose={
onClose} open={
isOpen} width="100%">
<h1>Project Modal</h1>
<Button onClick={
onClose}>关闭</Button>
</Drawer>
}
New src\screens\ProjectList\components\ProjectPopover.tsx
:
import styled from "@emotion/styled"
import {
Divider, List, Popover, Typography } from "antd"
import {
ButtonNoPadding } from "components/lib"
import {
useProjects } from "utils/project"
export const ProjectPopover = ({
setIsOpen }: {
setIsOpen: (isOpen: boolean) => void }) => {
const {
data: projects } = useProjects()
const starProjects = projects?.filter(i => i.star)
const content = <ContentContainer>
<Typography.Text type="secondary">收藏项目</Typography.Text>
<List>
{
starProjects?.map(project => <List.Item>
<List.Item.Meta title={
project.name}/>
</List.Item>)
}
</List>
<Divider/>
<ButtonNoPadding type='link' onClick={
() => setIsOpen(true)}>创建项目</ButtonNoPadding>
</ContentContainer>
return <Popover placement="bottom" content={
content}>
项目
</Popover>
}
const ContentContainer = styled.div`
width: 30rem;
`
Edit src\authenticated-app.tsx
(introduce ButtonNoPadding
, ProjectPopover
, ProjectModal
custom components, and pass the state management method of the modal box to the corresponding component PageHeader
and ProjectList
, note that the receiver must define the type):
...
import {
ButtonNoPadding, Row } from "components/lib";
...
import {
ProjectModal } from "screens/ProjectList/components/ProjectModal";
import {
useState } from "react";
import {
ProjectPopover } from "screens/ProjectList/components/ProjectPopover";
export const AuthenticatedApp = () => {
const [isOpen, setIsOpen] = useState(false)
...
return (
<Container>
<PageHeader setIsOpen={
setIsOpen}/>
<Main>
<Router>
<Routes>
<Route path="/projects" element={
<ProjectList setIsOpen={
setIsOpen}/>} />
...
</Routes>
</Router>
</Main>
<ProjectModal isOpen={
isOpen} onClose={
() => setIsOpen(false)}/>
</Container>
);
};
const PageHeader = ({
setIsOpen }: {
setIsOpen: (isOpen: boolean) => void }) => {
...
return (
<Header between={
true}>
<HeaderLeft gap={
true}>
<ButtonNoPadding type="link" onClick={
resetRoute}>
<SoftwareLogo width="18rem" color="rgb(38,132,255)" />
</ButtonNoPadding>
<ProjectPopover setIsOpen={
setIsOpen}/>
<span>用户</span>
</HeaderLeft>
<HeaderRight>
...
</HeaderRight>
</Header>
);
};
...
Since multiple components will initiate calls after login,
ProjectModal
the component needs to be placedAuthenticatedApp
underContainer
the
EDIT src\screens\ProjectList\index.tsx
(to introduce state management methods for modal boxes):
...
import {
Row, Typography } from "antd";
...
import {
ButtonNoPadding } from "components/lib";
export const ProjectList = ({
setIsOpen }: {
setIsOpen: (isOpen: boolean) => void }) => {
...
return (
<Container>
<Row justify='space-between'>
<h1>项目列表</h1>
<ButtonNoPadding type='link' onClick={
() => setIsOpen(true)}>创建项目</ButtonNoPadding>
</Row>
...
<List
setIsOpen={
setIsOpen}
{
...}
/>
</Container>
);
};
...
EDIT src\screens\ProjectList\components\List.tsx
(to introduce state management methods for modal boxes):
import {
Dropdown, MenuProps, Table, TableProps } from "antd";
...
import {
ButtonNoPadding } from "components/lib";
...
interface ListProps extends TableProps<Project> {
...
setIsOpen: (isOpen: boolean) => void;
}
export const List = ({
users, setIsOpen, ...props }: ListProps) => {
...
return (
<Table
pagination={
false}
columns={
[
...
{
render: (text, project) => {
const items: MenuProps["items"] = [
{
key: 'edit',
label: "编辑",
onClick: () => setIsOpen(true)
},
];
return <Dropdown menu={
{
items }}>
<ButtonNoPadding type="link" onClick={
(e) => e.preventDefault()}>...</ButtonNoPadding>
</Dropdown>
}
}
]}
{
...props}
></Table>
);
};
prop drilling
It can be clearly seen that if the state promotion ( ) of this method has a large number of interval layers (the definition and use are too far apart), not only the "drill-down" problem, but also the coupling degree is too high
The following uses component composition to decouple
Edit src\authenticated-app.tsx
(pass the modal box opening method bound ButtonNoPadding
as an attribute to the component that needs to be used):
...
export const AuthenticatedApp = () => {
...
return (
<Container>
<PageHeader projectButton={
<ButtonNoPadding type="link" onClick={
() => setIsOpen(true)}>
创建项目
</ButtonNoPadding>
} />
<Main>
<Router>
<Routes>
<Route
path="/projects"
element={
<ProjectList projectButton={
<ButtonNoPadding type="link" onClick={
() => setIsOpen(true)}>
创建项目
</ButtonNoPadding>
} />}
/>
...
</Routes>
</Router>
</Main>
...
</Container>
);
};
const PageHeader = (props: {
projectButton: JSX.Element }) => {
...
return (
<Header between={
true}>
<HeaderLeft gap={
true}>
...
<ProjectPopover {
...props } />
...
</HeaderLeft>
<HeaderRight>...</HeaderRight>
</Header>
);
};
...
EDIT src\screens\ProjectList\components\ProjectPopover.tsx
(use the passed-in attribute component instead of the previous one bound to the modal open method ButtonNoPadding
):
...
export const ProjectPopover = ({
projectButton }: {
projectButton: JSX.Element }) => {
...
const content = (
<ContentContainer>
...
{
projectButton }
</ContentContainer>
);
...
};
...
EDIT src\screens\ProjectList\index.tsx
(use the passed-in attribute component instead of the previous bound modal box opening method ButtonNoPadding
and continue to "drill down"):
...
export const ProjectList = ({
projectButton }: {
projectButton: JSX.Element }) => {
...
return (
<Container>
<Row justify="space-between">
...
{
projectButton }
</Row>
...
<List
projectButton={
projectButton}
{
...}
/>
</Container>
);
};
...
EDIT src\screens\ProjectList\components\List.tsx
(use the passed-in attribute component instead of the previous one bound to the modal open method ButtonNoPadding
):
...
interface ListProps extends TableProps<Project> {
...
projectButton: JSX.Element
}
// type PropsType = Omit<ListProps, 'users'>
export const List = ({
users, ...props }: ListProps) => {
...
return (
<Table
pagination={
false}
columns={
[
...
{
render: (text, project) => {
return (
<Dropdown
dropdownRender={
() => props.projectButton}>
<ButtonNoPadding
type="link"
onClick={
(e) => e.preventDefault()}
>
...
</ButtonNoPadding>
</Dropdown>
);
},
},
]}
{
...props}
></Table>
);
};
- The edit button is not appropriate to use here, but this is not the final solution, just understand the idea
- Analysis of inversion of control - Zhihu
Some reference notes are still in draft stage, so stay tuned. . .