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
- 10. Use react-query to get data and manage cache
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
10. Use react-query to get data and manage cache
1~3
4. Edit and add project function
Next, use react-query
the transformation editing and adding project functions, and add to get project detailshook
editsrc\utils\project.ts
...
import {
useMutation, useQuery, useQueryClient } from "react-query";
export const useEditProject = () => {
const client = useHttp();
const queryClient = useQueryClient();
return useMutation(
(params: Partial<Project>) =>
client(`projects/${
params.id}`, {
method: "PATCH",
data: params,
}),
{
onSuccess: () => queryClient.invalidateQueries("projects"),
}
);
};
export const useAddProject = () => {
const client = useHttp();
const queryClient = useQueryClient();
return useMutation(
(params: Partial<Project>) =>
client(`projects`, {
method: "POST",
data: params,
}),
{
onSuccess: () => queryClient.invalidateQueries("projects"),
}
);
};
export const useProject = (id?: number) => {
const client = useHttp();
return useQuery<Project>(['project', id], () => client(`projects/${
id}`), {
enabled: Boolean(id) });
};
- invalidateQueries: Expires the data of the specified key (if not specified, all expire)
- useMutation: handle write operations, such as create/update/delete, etc.;
- useIsMutation and useIsFetching: Whether there is a fetch request or a mutation request in progress in the application.
Next, improve the new/edit modal box
EDIT src\screens\ProjectList\components\ProjectModal.tsx
(was equivalent to an empty page before):
import {
Button, Drawer, Form, Input, Spin } from "antd";
import {
useProjectModal } from "../utils";
import {
UserSelect } from "components/user-select";
import {
useAddProject, useEditProject } from "utils/project";
import {
useForm } from "antd/lib/form/Form";
import {
useEffect } from "react";
import {
ErrorBox } from "components/lib";
import styled from "@emotion/styled";
export const ProjectModal = () => {
const {
projectModalOpen, close, editingProject, isLoading } = useProjectModal();
const useMutateProject = editingProject ? useEditProject : useAddProject
const {
mutateAsync, error, isLoading: mutateLoading} = useMutateProject()
const [form] = useForm()
const onFinish = (values: any) => {
mutateAsync({
...editingProject, ...values}).then(() => {
form.resetFields()
close()
})
}
const title = editingProject ? '编辑项目' : '创建项目'
useEffect(() => {
form.setFieldsValue(editingProject)
return () => form.resetFields()
}, [editingProject, form])
return (
<Drawer forceRender onClose={
close} open={
projectModalOpen} width="100%">
<Container>
{
isLoading ? <Spin size="large"/> : <>
<h1>{
title}</h1>
<ErrorBox error={
error}/>
<Form form={
form} layout="vertical" style={
{
width: '40rem'}} onFinish={
onFinish}>
<Form.Item label='名称' name='name' rules={
[{
required: true, message: '请输入项目名称'}]}>
<Input placeholder="请输入项目名称"/>
</Form.Item>
<Form.Item label='部门' name='organization' rules={
[{
required: true, message: '请输入部门名称'}]}>
<Input placeholder="请输入部门名称"/>
</Form.Item>
<Form.Item label='负责人' name='personId'>
<UserSelect defaultOptionName="负责人"/>
</Form.Item>
<Form.Item style={
{
textAlign: 'right'}}>
<Button loading={
mutateLoading} type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
</>
}
</Container>
</Drawer>
);
};
const Container = styled.div`
height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`
Notice:
Drawer
When usinguseForm()
the extractedform
binding in the componentForm
, you need to add the property, otherwise an errorforceRender
will be reported if the binding is not found when the page is opened. any Form element. Forget...useEditProject
The parameters areuseAddProject
the same and mutually exclusive, so they can be combined into onehook
using- In the video, after editing and closing the modal box, the form will be cleared. The blogger here has done the
useEffect
processingcallback
in advance.
Since the person in charge is an optional item, the parameters of UserSelect
the subcomponent of the corresponding component IdSelect
are changed to optional src\components\id-select.tsx
:
...
// 类型不是简单的后来者居上,而是寻求"最大公约数"的方式
interface IdSelectProps extends Omit<SelectProps, "value" | "onChange" | "options"> {
value?: SN | null | undefined;
onChange?: (value?: number) => void;
...
}
...
export const IdSelect = (props: IdSelectProps) => {
const {
value, onChange, defaultOptionName, options, ...restProps } = props;
return (
<Select
value={
options?.length ? toNumber(value) : 0}
onChange={
(value) => onChange?.(toNumber(value) || undefined)}
{
...restProps}
>
...
</Select>
);
};
...
The page is set up, and the next step is to improve the data logic
EDIT src\screens\ProjectList\utils.ts
:
import {
useMemo } from "react";
import {
useProject } from "utils/project";
import {
useUrlQueryParam } from "utils/url";
...
export const useProjectModal = () => {
const [{
projectCreate, editingProjectId }, setUrlParams] = useUrlQueryParam([
"projectCreate", 'editingProjectId'
]);
const {
data: editingProject, isLoading } = useProject(
Number(editingProjectId)
);
const open = () => setUrlParams({
projectCreate: true });
const close = () => setUrlParams({
projectCreate: false, editingProjectId: undefined }); // 若在url显示 false,可以改为 undefined 隐藏
const startEdit = (id: number) => setUrlParams({
editingProjectId: id });
return {
projectModalOpen: projectCreate === "true" || Boolean(editingProjectId),
open,
close,
startEdit,
editingProject,
isLoading
};
};
Use the editing function to the List component to complete the last step
EDIT src\screens\ProjectList\components\List.tsx
:
...
export const List = ({
users, ...props }: ListProps) => {
const {
startEdit } = useProjectModal();
...
const editProject = (id: number) => () => startEdit(id);
return (
<Table
pagination={
false}
columns={
[
...
{
render: (text, project) => {
const items: MenuProps["items"] = [
{
key: "edit",
label: "编辑",
onClick: editProject(project.id),
},
...
];
...
},
},
]}
{
...props}
></Table>
);
};
View the page, add and edit functions are normal
5. Implement optimistic update with react-query
When the network situation is not very good, in order to improve the user experience, you can use " optimistic update ", that is, directly update the local cache data according to the user's wishes before the request succeeds, and automatically perform data rollback if the request fails
EDIT src\utils\project.ts
:
...
import {
useProjectsSearchParams } from "screens/ProjectList/utils";
...
export const useEditProject = () => {
const client = useHttp();
const queryClient = useQueryClient();
const [searchParams] = useProjectsSearchParams()
const queryKey = ['projects', searchParams]
return useMutation(
(params: Partial<Project>) =>
client(`projects/${
params.id}`, {
method: "PATCH",
data: params,
}),
{
onSuccess: () => queryClient.invalidateQueries(queryKey),
// async
onMutate: (target) => {
const previousItems = queryClient.getQueryData(queryKey)
queryClient.setQueryData(queryKey, (old: Project[] = []) => {
return old?.map(project => project.id === target.id ? {
...project, ...target } : project)
})
return {
previousItems}
},
onError: (error, newItem, context) => {
queryClient.setQueryData(queryKey, context?.previousItems)
}
}
);
};
...
View the page, adjust the response time of the development tool 2000ms
, star a certain project, and check the effect;
then adjust the failure rate to 100%
, star a certain project again, and check the effect
The function is easy to use, but I found that a lot of code (the second parameter) of this part useMutation
can be reused, and then I will extract a special treatment for optimistic updatehook
New src\utils\use-optimistic-options.ts
:
import {
QueryKey, useQueryClient } from 'react-query'
export const useConfig = (queryKey: QueryKey, callback: (target: any, old?: any[]) => any[]) => {
const queryClient = useQueryClient()
return {
onSuccess: () => queryClient.invalidateQueries(queryKey),
// async
onMutate: (target: any) => {
const previousItems = queryClient.getQueryData(queryKey)
queryClient.setQueryData(queryKey, (old?: any[]) => callback(target, old))
return {
previousItems}
},
onError: (error: any, newItem: any, context: any) => {
queryClient.setQueryData(queryKey, context?.previousItems)
}
}
}
export const useDeleteConfig = (queryKey: QueryKey) =>
useConfig(queryKey, (id: any, old?:any[]) => old?.filter(item => item.id !== id) || [])
export const useEditConfig = (queryKey: QueryKey) =>
useConfig(queryKey, (target: any, old?:any[]) => old?.map(item => item.id === target.id ? {
...item, ...target } : item) || [])
export const useAddConfig = (queryKey: QueryKey) =>
useConfig(queryKey, (target: any, old?:any[]) => old ? [...old, target] : [])
- Since
react-query
the type mechanism of is relatively complex, mandatory adaptation is not cost-effective, and this part is highly independent, so multiple types are usedany
Because the parameters of the item list search useProjectsSearchParams
are ProjectList
bound to the page, and useEditProject
this is likely to be used on other pages, in order to maintain its versatility, queryKey
it is extracted separately and passed in as a parameter when used
editsrc\screens\ProjectList\utils.ts
...
import {
QueryKey, useMutation, useQuery } from "react-query";
import {
useAddConfig, useEditConfig } from "./use-optimistic-options";
...
export const useEditProject = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
...,
useEditConfig(queryKey)
);
};
export const useAddProject = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
...,
useAddConfig(queryKey)
);
};
...
editsrc\screens\ProjectList\utils.ts
...
export const useProjectQueryKey = () => ['projects', useProjectsSearchParams()[0]]
...
Pass in where it is usedqueryKey
editsrc\screens\ProjectList\components\List.tsx
...
import {
useProjectModal, useProjectQueryKey } from "../utils";
...
export const List = (...) => {
...
const {
mutate } = useEditProject(useProjectQueryKey());
...
};
editsrc\screens\ProjectList\components\ProjectModal.tsx
...
import {
useProjectModal, useProjectQueryKey } from "../utils";
export const ProjectModal = () => {
...
const {
mutateAsync, error, isLoading: mutateLoading } = useMutateProject(useProjectQueryKey());
...
};
...
Test the function again, it works fine!
Next, complete the delete function
editsrc\utils\project.ts
...
import {
useAddConfig, useDeleteConfig, useEditConfig } from "./use-optimistic-options";
...
export const useDeleteProject = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`projects/${
id}`, {
method: "DELETE"
}),
useDeleteConfig(queryKey)
);
};
...
editsrc\screens\ProjectList\components\List.tsx
...
import {
Dropdown, MenuProps, Modal, Table, TableProps } from "antd";
import {
useProjectModal, useProjectQueryKey } from "../utils";
...
export const List = ({
users, ...props }: ListProps) => {
const {
mutate } = useEditProject(useProjectQueryKey());
// 函数式编程 柯里化
const starProject = (id: number) => (star: boolean) => mutate({
id, star });
return (
<Table
pagination={
false}
columns={
[
...
{
render: (text, project) => <More project={
project}/>
},
]}
{
...props}
></Table>
);
};
const More = ({
project}: {
project: Project}) => {
const {
startEdit } = useProjectModal();
const editProject = (id: number) => () => startEdit(id);
const {
mutate: deleteProject} = useDeleteProject(useProjectQueryKey())
const confirmDeleteProject = (id: number) => {
Modal.confirm({
title: '确定删除这个项目吗?',
content: '点击确定删除',
onOk: () => deleteProject(id)
})
}
const items: MenuProps["items"] = [
{
key: "edit",
label: "编辑",
onClick: editProject(project.id),
},
{
key: "delete",
label: "删除",
onClick: () => confirmDeleteProject(project.id),
},
];
return (
<Dropdown menu={
{
items }}>
<ButtonNoPadding
type="link"
onClick={
(e) => e.preventDefault()}
>
...
</ButtonNoPadding>
</Dropdown>
);
}
- Edit and delete this part of the function can be extracted separately
- Note that it is not passed in according to the deletion in the video
{id}
, but is used directly in useDeleteConfigid
(target
)
Delete a test function (Note: testing optimistic updates requires adjusting the development tool response time!)
Added optimistic updates are not obvious
6. Separately extract URL parameter logic
According to the previous writing method of the video, there is a problem, so here we need to separate the extraction url
parameter logic separately
editsrc\utils\url.ts
...
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
const [searchParams] = useSearchParams();
const setSearchParams = useSetUrlSearchParam()
return [
useMemo(
() =>
keys.reduce((prev, key) => {
// searchParams.get 可能会返回 null,需要预设值来兼容
return {
...prev, [key]: searchParams.get(key) || "" };
// 初始值会对类型造成影响,需要手动指定
}, {
} as {
[key in K]: string }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchParams]
),
(params: Partial<{
[key in K]: unknown }>) => setSearchParams(params)
] as const;
};
export const useSetUrlSearchParam = () => {
const [searchParams, setSearchParams] = useSearchParams();
return (params: {
[key in string]: unknown}) => {
const o = cleanObject({
...Object.fromEntries(searchParams),
...params,
}) as URLSearchParamsInit;
return setSearchParams(o);
}
}
editsrc\screens\ProjectList\utils.ts
...
import {
useSetUrlSearchParam, useUrlQueryParam } from "utils/url";
...
export const useProjectModal = () => {
const [{
projectCreate, editingProjectId }] = useUrlQueryParam([
"projectCreate",
"editingProjectId",
]);
const setUrlParams = useSetUrlSearchParam()
const {
data: editingProject, isLoading } = useProject(
Number(editingProjectId)
);
const open = () => setUrlParams({
projectCreate: true });
const close = () =>
setUrlParams({
projectCreate: false, editingProjectId: undefined }); // 若在url显示 false,可以改为 undefined 隐藏
...
};
In fact, according to the blogger’s understanding, the key point is not to separate, but to set
setUrlParams
the timeprojectCreate
andeditingProjectId
time at the same time, instead of setting them one by one according to the previous video (it still feels weird, I don’t know if this is the reason why the previous video operationprojectCreate
could not be set to )false
Finally, remember to clear the cache after logging out react-query
to avoid the bug that the old data is still displayed before the new data request comes back after the next user logs in
EDIT src\context\auth-context.tsx
:
...
import {
useQueryClient } from "react-query";
...
export const AuthProvider = ({
children }: {
children: ReactNode }) => {
...
const queryClient = useQueryClient()
...
const logout = () => auth.logout().then(() => {
setUser(null)
queryClient.clear()
});
...
};
7. Summary of cross-component state management solutions
small scene
- State promotion, composite components
cache status
- react-query、swr
client status
- url、redux、context
Some reference notes are still in draft stage, so stay tuned. . .