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
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-toolkit
It is redux
the 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
state
values to the attributes of ?
Answer:redux
Use the built-inimmer
to 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
hook
to getdispatch
:const dispatch = useDispatch()
dispatch
Open the modal using the call:() => dispatch(projectListActions.openProjectModal())
- Use
dispatch
the call to close the modal:() => dispatch(projectListActions.closeProjectModal())
- Use to
hook
get the current opening and closing state of the modal box:useSelector(selectProjectModalOpen)
useSelector
Used 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>;
ReturnType
The 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 redux
the dispatch
of 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 redux
bound store
to the context
global
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. . .