[Practice] 9. In-depth React state management and Redux mechanism (4) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (19)


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&2

3&4

5~8

9. Configure redux-toolkit

redux-toolkitIt is reduxthe secondary package of , which mainly solves three major pain points:

  • Complex configuration
  • Too many packages to add
  • Requires too much boilerplate code

Since the project will not be used in the end redux, a new branch will be opened for learning and development, and a branch will be createdredux-toolkit

Installation dependencies:

npm i react-redux @reduxjs/toolkit # --force

New 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>

New 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) {
    
    }
  }
})

Q: Why can I directly assign statevalues ​​to the attributes of ?
Answer: reduxUse the built-in immerto process it into immutable data, create a " shadow state " and finally replace the original state as a whole

10. Apply redux-toolkit management modal box

perfect 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

Subsequent use:

  • introduce
    • import { useDispatch, useSelector } from "react-redux";
    • import { projectListActions, selectProjectModalOpen } from "../projectList.slice";
  • Use hookto get dispatch:const dispatch = useDispatch()
  • dispatchOpen the modal using the call:() => dispatch(projectListActions.openProjectModal())
  • Use dispatchthe call to close the modal:() => dispatch(projectListActions.closeProjectModal())
  • Use to hookget the current opening and closing state of the modal box:useSelector(selectProjectModalOpen)
    • useSelectorUsed to read the root state tree

Modification src\store\index.tsx(introduction 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>;
  • ReturnTypeThe type used to read the return value of the function

Before AuthenticatedApp(the place where the sub-encapsulation modal box component is introduced), the state quantity ( const [isOpen, setIsOpen] = useState(false)) was created layer by layer and passed to the place where the modal box is used

That's all for now, let's start using reduxthe dispatchof to change the state of the modal:

EDIT 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>
  );
};
...

EDIT 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>
  );
};
...

EDIT 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>
  );
};

EDIT 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>
  );
};

EDIT 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>
  );
  ...
};
...

Now when you visit the page, you will find an error:

could not find react-redux context value; please ensure the component is wrapped in a <Provider>

This is because the of is not reduxbound storeto the contextglobal

EDIT 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>
  );
};

Visit the page again, the function is OK


Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/132126643