學(xué)習(xí)內(nèi)容來(lái)源:React + React Hook + TS 最佳實(shí)踐-慕課網(wǎng)
相對(duì)原教程,我在學(xué)習(xí)開(kāi)始時(shí)(2023.03)采用的是當(dāng)前最新版本:
項(xiàng) | 版本 |
---|---|
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 |
具體配置、操作和內(nèi)容會(huì)有差異,“坑”也會(huì)有所不同。。。
一、項(xiàng)目起航:項(xiàng)目初始化與配置
- 一、項(xiàng)目起航:項(xiàng)目初始化與配置
二、React 與 Hook 應(yīng)用:實(shí)現(xiàn)項(xiàng)目列表
- 二、React 與 Hook 應(yīng)用:實(shí)現(xiàn)項(xiàng)目列表
三、TS 應(yīng)用:JS神助攻 - 強(qiáng)類(lèi)型
- 三、 TS 應(yīng)用:JS神助攻 - 強(qiáng)類(lèi)型
四、JWT、用戶認(rèn)證與異步請(qǐng)求
- 四、 JWT、用戶認(rèn)證與異步請(qǐng)求(上)
- 四、 JWT、用戶認(rèn)證與異步請(qǐng)求(下)
五、CSS 其實(shí)很簡(jiǎn)單 - 用 CSS-in-JS 添加樣式
- 五、CSS 其實(shí)很簡(jiǎn)單 - 用 CSS-in-JS 添加樣式(上)
- 五、CSS 其實(shí)很簡(jiǎn)單 - 用 CSS-in-JS 添加樣式(下)
六、用戶體驗(yàn)優(yōu)化 - 加載中和錯(cuò)誤狀態(tài)處理
- 六、用戶體驗(yàn)優(yōu)化 - 加載中和錯(cuò)誤狀態(tài)處理(上)
- 六、用戶體驗(yàn)優(yōu)化 - 加載中和錯(cuò)誤狀態(tài)處理(中)
- 六、用戶體驗(yàn)優(yōu)化 - 加載中和錯(cuò)誤狀態(tài)處理(下)
七、Hook,路由,與 URL 狀態(tài)管理
- 七、Hook,路由,與 URL 狀態(tài)管理(上)
- 七、Hook,路由,與 URL 狀態(tài)管理(中)
- 七、Hook,路由,與 URL 狀態(tài)管理(下)
八、用戶選擇器與項(xiàng)目編輯功能
- 八、用戶選擇器與項(xiàng)目編輯功能(上)
- 八、用戶選擇器與項(xiàng)目編輯功能(下)
九、深入React 狀態(tài)管理與Redux機(jī)制
1.useCallback應(yīng)用,優(yōu)化異步請(qǐng)求
當(dāng)前項(xiàng)目中使用 useAsync
進(jìn)行異步請(qǐng)求,但是其中有一個(gè)隱藏 bug
,若是在頁(yè)面中發(fā)起一個(gè)請(qǐng)求,這個(gè)請(qǐng)求需要較長(zhǎng)時(shí)間3s(可以使用開(kāi)發(fā)控制臺(tái)設(shè)置請(qǐng)求最短時(shí)間來(lái)預(yù)設(shè)場(chǎng)景),在這個(gè)時(shí)間段內(nèi),退出登錄,此時(shí)就會(huì)有報(bào)錯(cuò):
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.
原因是雖然退出登錄,組件銷(xiāo)毀,但是異步函數(shù)還在執(zhí)行,當(dāng)它執(zhí)行完進(jìn)行下一步操作 setXXX
或是 更新組件都找不到對(duì)應(yīng)已銷(xiāo)毀的組件。
接下來(lái)解決一下這個(gè)問(wèn)題。
編輯 src\utils\index.ts
:
...
/**
* 返回組件的掛載狀態(tài),如果還沒(méi)有掛載或者已經(jīng)卸載,返回 false; 反之,返回 true;
*/
export const useMountedRef = () => {
const mountedRef = useRef(false)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return mountedRef
}
在 src\utils\use-async.ts
上應(yīng)用:
...
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) => {...});
};
...
};
還有個(gè)遺留問(wèn)題,在 useEffect
中使用的變量若是沒(méi)有在依賴(lài)數(shù)組中添加就會(huì)報(bào)錯(cuò),添加上又會(huì)造成死循環(huán),因此之前用 eslint-disable-next-line
解決
// eslint-disable-next-line react-hooks/exhaustive-deps
現(xiàn)在換個(gè)方案,使用 useMemo
當(dāng)然可以解決,這里推薦使用特殊版本的 useMemo
, useCallback
:
修改 src\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 來(lái)觸發(fā)異步請(qǐng)求
const run = useCallback((...) => {
...
}, [config.throwOnError, mountedRef, setData, state, setError],
)
...
};
可以按照提示配置依賴(lài):
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
盡管如此,但還是難免會(huì)出現(xiàn),在 useCallback
中改變 依賴(lài)值的行為,比如依賴(lài)值 XXX
對(duì)應(yīng)的 setXXX
,這時(shí)需要用到 setXXX
的函數(shù)用法(這樣也可以省去一個(gè)依賴(lài)):
繼續(xù)修改 src\utils\use-async.ts
...
export const useAsync = <D>(...) => {
...
const run = useCallback((...) => {
...
setState(prevState => ({ ...prevState, stat: "loading" }));
...
}, [config.throwOnError, mountedRef, setData, setError],
)
...
};
修改 src\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]);
...
};
...
修改 src\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]);
};
總結(jié):非狀態(tài)類(lèi)型需要作為依賴(lài) 就要將其使用 useMemo
或者 useCallback
包裹(依賴(lài)細(xì)化 + 新舊關(guān)聯(lián)),常見(jiàn)于 Custom Hook
中函數(shù)類(lèi)型數(shù)據(jù)的返回
2.狀態(tài)提升,組合組件與控制反轉(zhuǎn)
接下來(lái)定制化一個(gè)項(xiàng)目編輯模態(tài)框(編輯+新建項(xiàng)目),PageHeader
hover
后可以打開(kāi)(新建),ProjectList
中可以打開(kāi)模態(tài)框(新建),里面的 List
的每行也可以打開(kāi)模態(tài)框(編輯)
在 src\components\lib.tsx
中新增 padding
為 0
的 Button
:
...
export const ButtonNoPadding = styled(Button)`
padding: 0;
`
新建 src\screens\ProjectList\components\ProjectModal.tsx
(模態(tài)框):
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}>關(guān)閉</Button>
</Drawer>
}
新建 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">收藏項(xiàng)目</Typography.Text>
<List>
{
starProjects?.map(project => <List.Item>
<List.Item.Meta title={project.name}/>
</List.Item>)
}
</List>
<Divider/>
<ButtonNoPadding type='link' onClick={() => setIsOpen(true)}>創(chuàng)建項(xiàng)目</ButtonNoPadding>
</ContentContainer>
return <Popover placement="bottom" content={content}>
項(xiàng)目
</Popover>
}
const ContentContainer = styled.div`
width: 30rem;
`
編輯 src\authenticated-app.tsx
(引入 ButtonNoPadding
、 ProjectPopover
、 ProjectModal
自定義組件,并將模態(tài)框的狀態(tài)管理方法傳到對(duì)應(yīng)組件 PageHeader
和 ProjectList
,注意接收方要定義好類(lèi)型):
...
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>
);
};
...
由于涉及登錄后多個(gè)組件會(huì)發(fā)起調(diào)用,因此
ProjectModal
組件需要放在AuthenticatedApp
的Container
下
編輯 src\screens\ProjectList\index.tsx
(引入 模態(tài)框的狀態(tài)管理方法):
...
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>項(xiàng)目列表</h1>
<ButtonNoPadding type='link' onClick={() => setIsOpen(true)}>創(chuàng)建項(xiàng)目</ButtonNoPadding>
</Row>
...
<List
setIsOpen={setIsOpen}
{...}
/>
</Container>
);
};
...
編輯 src\screens\ProjectList\components\List.tsx
(引入 模態(tài)框的狀態(tài)管理方法):
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>
);
};
可以明顯看到,這種方式的狀態(tài)提升(prop drilling
)若是間隔層數(shù)較多時(shí)(定義和使用相隔太遠(yuǎn)),不僅有“下鉆”問(wèn)題,而且耦合度太高
下面使用 組件組合(component composition)的方式解耦
組件組合(component composition) | Context – React
編輯 src\authenticated-app.tsx
(將 綁定了模態(tài)框 打開(kāi)方法的 ButtonNoPadding
作為屬性傳給需要用到的組件):
...
export const AuthenticatedApp = () => {
...
return (
<Container>
<PageHeader projectButton={
<ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>
創(chuàng)建項(xiàng)目
</ButtonNoPadding>
} />
<Main>
<Router>
<Routes>
<Route
path="/projects"
element={<ProjectList projectButton={
<ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>
創(chuàng)建項(xiàng)目
</ButtonNoPadding>
} />}
/>
...
</Routes>
</Router>
</Main>
...
</Container>
);
};
const PageHeader = (props: { projectButton: JSX.Element }) => {
...
return (
<Header between={true}>
<HeaderLeft gap={true}>
...
<ProjectPopover { ...props } />
...
</HeaderLeft>
<HeaderRight>...</HeaderRight>
</Header>
);
};
...
編輯 src\screens\ProjectList\components\ProjectPopover.tsx
(使用傳入的屬性組件代替之前的 綁定了模態(tài)框 打開(kāi)方法的 ButtonNoPadding
):
...
export const ProjectPopover = ({ projectButton }: { projectButton: JSX.Element }) => {
...
const content = (
<ContentContainer>
...
{ projectButton }
</ContentContainer>
);
...
};
...
編輯 src\screens\ProjectList\index.tsx
(使用傳入的屬性組件代替之前的 綁定了模態(tài)框 打開(kāi)方法的 ButtonNoPadding
并繼續(xù)“下鉆”):
...
export const ProjectList = ({ projectButton }: { projectButton: JSX.Element }) => {
...
return (
<Container>
<Row justify="space-between">
...
{ projectButton }
</Row>
...
<List
projectButton={projectButton}
{...}
/>
</Container>
);
};
...
編輯 src\screens\ProjectList\components\List.tsx
(使用傳入的屬性組件代替之前的 綁定了模態(tài)框 打開(kāi)方法的 ButtonNoPadding
):文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-616401.html
...
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>
);
};
- 編輯按鈕這里使用并不恰當(dāng),不過(guò)這不是最終解決方案,理解思路即可
- 淺析控制反轉(zhuǎn) - 知乎
部分引用筆記還在草稿階段,敬請(qǐng)期待。。。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-616401.html
到了這里,關(guān)于【實(shí)戰(zhàn)】 九、深入React 狀態(tài)管理與Redux機(jī)制(一) —— React17+React Hook+TS4 最佳實(shí)踐,仿 Jira 企業(yè)級(jí)項(xiàng)目(十六)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!