文章目录
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
项 | 版本 |
---|---|
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 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
六、用户体验优化 - 加载中和错误状态处理
七、Hook,路由,与 URL 状态管理
八、用户选择器与项目编辑功能
九、深入React 状态管理与Redux机制
1&2
3&4
5~8
9.配置redux-toolkit
redux-toolkit
是对 redux
的二次封装,主要解决三大痛点:
- 配置复杂
- 需要增加的包太多
- 需要太多模板代码
由于项目最终不会使用到 redux
,因此接下来新开一个分支用作学习开发,创建分支 redux-toolkit
安装依赖:
npm i react-redux @reduxjs/toolkit # --force
新建 src\store\index.tsx
:
import {
configureStore } from "@reduxjs/toolkit"
export const rootReducer = {
}
export const store = configureStore({
reducer: rootReducer
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
新建 src\screens\ProjectList\projectList.slice.ts
:
import {
createSlice } from "@reduxjs/toolkit";
interface State {
projectModalOpen: boolean;
}
const initialState: State = {
projectModalOpen: false
}
export const projectListSlice = createSlice({
name: 'projectListSlice',
initialState,
reducers: {
openProjectModal(state, action) {
},
closeProjectModal(state, action) {
}
}
})
问:为什么这里可以直接给
state
的属性赋值?
答:redux
借助内置的immer
来处理使其变为不可变数据的同时,创建“影子状态”最终整体替换原状态
10.应用 redux-toolkit 管理模态框
完善 src\screens\ProjectList\projectList.slice.ts
:
import {
createSlice } from "@reduxjs/toolkit";
import {
RootState } from "store";
interface State {
projectModalOpen: boolean;
}
const initialState: State = {
projectModalOpen: false,
};
export const projectListSlice = createSlice({
name: "projectListSlice",
initialState,
reducers: {
openProjectModal(state) {
state.projectModalOpen = true
},
closeProjectModal(state) {
state.projectModalOpen = false
},
},
});
export const projectListActions = projectListSlice.actions;
export const selectProjectModalOpen = (state: RootState) => state.projectList.projectModalOpen
后续使用方式:
- 引入
import { useDispatch, useSelector } from "react-redux";
import { projectListActions, selectProjectModalOpen } from "../projectList.slice";
- 使用
hook
拿到dispatch
:const dispatch = useDispatch()
- 使用
dispatch
调用打开模态框:() => dispatch(projectListActions.openProjectModal())
- 使用
dispatch
调用关闭模态框:() => dispatch(projectListActions.closeProjectModal())
- 使用
hook
获取模态框当前开闭状态:useSelector(selectProjectModalOpen)
useSelector
用来读根状态树
修改 src\store\index.tsx
(引入 projectListSlice
):
import {
configureStore } from "@reduxjs/toolkit";
import {
projectListSlice } from "screens/ProjectList/projectList.slice";
export const rootReducer = {
projectList: projectListSlice.reducer,
};
export const store = configureStore({
reducer: rootReducer,
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
ReturnType
用来读取函数返回值的类型
之前在 AuthenticatedApp
(子封装模态框组件引入的地方) 创建状态量(const [isOpen, setIsOpen] = useState(false)
)分别一层一层传入使用到模态框的地方
现在不用啦,接下来开始使用 redux
的 dispatch
更改模态框状态:
编辑 src\authenticated-app.tsx
:
...
export const AuthenticatedApp = () => {
- const [isOpen, setIsOpen] = useState(false);
...
return (
<Container>
- <PageHeader
- projectButton={
- <ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>
- 创建项目
- </ButtonNoPadding>
- }
- />
+ <PageHeader/>
<Main>
<Router>
<Routes>
<Route
path="/projects"
element={
- <ProjectList
- projectButton={
- <ButtonNoPadding
- type="link"
- onClick={() => setIsOpen(true)}
- >
- 创建项目
- </ButtonNoPadding>
- }
- />
+ <ProjectList/>
}
/>
...
</Routes>
</Router>
</Main>
- <ProjectModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
+ <ProjectModal/>
</Container>
);
};
- const PageHeader = (props: { projectButton: JSX.Element }) => {
+ const PageHeader = () => {
...
return (
<Header between={true}>
<HeaderLeft gap={true}>
...
- <ProjectPopover {...props} />
+ <ProjectPopover/>
<span>用户</span>
</HeaderLeft>
...
</Header>
);
};
...
编辑 src\screens\ProjectList\index.tsx
:
+ import { useDispatch } from "react-redux";
+ import { ButtonNoPadding } from "components/lib";
+ import { projectListActions } from "./projectList.slice";
- export const ProjectList = ({
- projectButton,
- }: {
- projectButton: JSX.Element;
- }) => {
+ export const ProjectList = () => {
...
+ const dispatch = useDispatch()
return (
<Container>
<Row justify="space-between">
<h1>项目列表</h1>
- {projectButton}
+ <ButtonNoPadding type="link" onClick={() => dispatch(projectListActions.openProjectModal())}>
+ 创建项目
+ </ButtonNoPadding>
</Row>
...
<List
- projectButton={projectButton}
...
/>
</Container>
);
};
...
编辑 src\screens\ProjectList\components\List.tsx
:
...
+ import { useDispatch } from "react-redux";
+ import { projectListActions } from "../projectList.slice";
interface ListProps extends TableProps<Project> {
users: User[];
refresh?: () => void;
- projectButton: JSX.Element;
}
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
+ const dispatch = useDispatch()
...
return (
<Table
pagination={false}
columns={[
...
{
render: (text, project) => {
+ const items: MenuProps["items"] = [
+ {
+ key: "edit",
+ label: "编辑",
+ onClick: () => dispatch(projectListActions.openProjectModal()),
+ },
+ ];
return (
- <Dropdown dropdownRender={() => props.projectButton}>
+ <Dropdown menu={
{items}}>
...
</Dropdown>
);
},
},
]}
{...props}
></Table>
);
};
编辑 src\screens\ProjectList\components\ProjectModal.tsx
:
...
+ import { useDispatch, useSelector } from "react-redux";
+ import { projectListActions, selectProjectModalOpen } from "../projectList.slice";
- export const ProjectModal = ({
- isOpen,
- onClose,
- }: {
- isOpen: boolean;
- onClose: () => void;
- }) => {
+ export const ProjectModal = () => {
+ const dispatch = useDispatch()
+ const projectModalOpen = useSelector(selectProjectModalOpen)
return (
- <Drawer onClose={onClose} open={isOpen} width="100%">
+ <Drawer
+ onClose={() => dispatch(projectListActions.closeProjectModal())}
+ open={projectModalOpen}
+ width="100%"
+ >
<h1>Project Modal</h1>
- <Button onClick={onClose}>关闭</Button>
+ <Button onClick={() => dispatch(projectListActions.closeProjectModal())}>关闭</Button>
</Drawer>
);
};
编辑 src\screens\ProjectList\components\ProjectPopover.tsx
:
...
+ import { useDispatch } from "react-redux";
+ import { projectListActions } from "../projectList.slice";
+ import { ButtonNoPadding } from "components/lib";
- export const ProjectPopover = ({
- projectButton,
- }: {
- projectButton: JSX.Element;
- }) => {
+ export const ProjectPopover = () => {
+ const dispatch = useDispatch()
...
const content = (
<ContentContainer>
<Typography.Text type="secondary">收藏项目</Typography.Text>
<List>
{starProjects?.map((project) => (
- <List.Item>
+ <List.Item key={project.id}>
<List.Item.Meta title={project.name} />
</List.Item>
))}
</List>
<Divider />
- {projectButton}
+ <ButtonNoPadding type="link" onClick={() => dispatch(projectListActions.openProjectModal())}>
+ 创建项目
+ </ButtonNoPadding>
</ContentContainer>
);
...
};
...
现在访问页面会发现有报错:
could not find react-redux context value; please ensure the component is wrapped in a <Provider>
这是因为没有将 redux
的 store
绑定到全局 context
上
编辑 src\context\index.tsx
:
...
+ import { store } from "store";
+ import { Provider } from "react-redux";
export const AppProvider = ({ children }: { children: ReactNode }) => {
return (
+ <Provider store={store}>
<QueryClientProvider client={new QueryClient()}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
+ </Provider>
);
};
再次访问页面,功能 OK 了
部分引用笔记还在草稿阶段,敬请期待。。。