【实战】 九、深入React 状态管理与Redux机制(四) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十九)


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(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 拿到 dispatchconst 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))分别一层一层传入使用到模态框的地方

现在不用啦,接下来开始使用 reduxdispatch 更改模态框状态:

编辑 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>

这是因为没有将 reduxstore 绑定到全局 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 了


部分引用笔记还在草稿阶段,敬请期待。。。

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/132126643