文章目录
-
- 一、项目起航:项目初始化与配置
- [二、React 与 Hook 应用:实现项目列表](#二、React 与 Hook 应用:实现项目列表)
- [三、TS 应用:JS神助攻 - 强类型](#三、TS 应用:JS神助攻 - 强类型)
- 四、JWT、用户认证与异步请求
- [五、CSS 其实很简单 - 用 CSS-in-JS 添加样式](#五、CSS 其实很简单 - 用 CSS-in-JS 添加样式)
- [六、用户体验优化 - 加载中和错误状态处理](#六、用户体验优化 - 加载中和错误状态处理)
- [七、Hook,路由,与 URL 状态管理](#七、Hook,路由,与 URL 状态管理)
- 八、用户选择器与项目编辑功能
- [九、深入React 状态管理与Redux机制](#九、深入React 状态管理与Redux机制)
相对原教程,我在学习开始时(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.合并组件状态,实现useUndo
功能描述:
可以对一个数字进行不断地赋值,同时记录下历史值;可以通过undo对当前值进行撤销操作,一步步地回到最初值。在进行撤销操作的同时,记录下undo掉的值;通过redo可以回到undo之前的值,不断地redo最终可以回到执行所有撤销操作之前的值。
代码实现
使用useState实现该hook,由于多个state值之前相互引用。因此useCallback的依赖项中会填入多个state作为依赖项,但是在useCallback的回调函数中,在调用时会更新state值导致页面重新渲染,useCallback的回调函数也被重新定义了。
接下来模仿实现 use-undo
新建 src\utils\use-undo.ts
:
js
import { useState } from "react";
export const useUndo = <T>(initialPresent: T) => {
// 记录历史操作的合集
const [past, setPast] = useState<T[]>([])
const [present, setPresent] = useState(initialPresent)
// 记录未来操作的合集
const [future, setFuture] = useState<T[]>([])
const canUndo = past.length !== 0
const canRedo = future.length !== 0
const undo = () => {
if (!canUndo) return
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
setPast(newPast)
setPresent(previous)
setFuture([present, ...future])
}
const redo = () => {
if (!canRedo) return
const next = future[0]
const newFuture = future.slice(1)
setFuture(newFuture)
setPresent(next)
setPast([...past, present])
}
const set = (newPresent: T) => {
if (newPresent === present) {
return
}
setPast([...past, present])
setPresent(newPresent)
setFuture([])
}
const reset = (newPresent: T) => {
setPast([])
setPresent(newPresent)
setFuture([])
}
return [
{past, present, future},
{redo, undo, set, reset, canRedo, canUndo}
] as const
}
现需要对代码做以下优化:
- 使用 useCallback 避免 造成之前的那种 依赖不等导致的循环渲染
- 将用到的 变量合并声明 后续也同步改动
- SetXXX 使用函数形式 直接使用历史状态 避免外界状态的使用,减少依赖
js
import { useCallback, useState } from "react";
export const useUndo = <T>(initialPresent: T) => {
// 合并声明
const [state, setState] = useState<{
past: T[],
present: T,
future: T[]
}>({
past: [],
present: initialPresent,
future: []
})
// 另一种写法
// const [state, setState] = useState({
// past: [] as T[],
// present: initialPresent as T,
// future: [] as T[]
// })
const canUndo = state.past.length !== 0
const canRedo = state.future.length !== 0
const undo = useCallback(() => {
setState(currentState => {
const { past, present, future } = currentState
if (past.length === 0) return currentState
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [present, ...future]
}
})
}, [])
const redo = useCallback(() => {
setState(currentState => {
const { past, present, future } = currentState
if (future.length === 0) return currentState
const next = future[0]
const newFuture = future.slice(1)
return {
past: [...past, present],
present: next,
future: newFuture
}
})
}, [])
const set = useCallback((newPresent: T) => {
setState(currentState => {
const { past, present } = currentState
if (newPresent === present) {
return currentState
}
return {
past: [...past, present],
present: newPresent,
future: []
}
})
}, [])
const reset = useCallback((newPresent: T) => {
setState({
past: [],
present: newPresent,
future: []
})
}, [])
return [
state,
{redo, undo, set, reset, canRedo, canUndo}
] as const
}
4.用useReducer进行状态管理
替代方案:
useReducer
作为useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
相比较于useState
,useReducer
具有如下优点:
state
中的状态值之间相互关联;- 下一个
state
的更新依赖于之前的state
。
下面使用 useReducer
再对 use-undo
进行改写
编辑 src\utils\use-undo.ts
:
js
import { useCallback, useReducer, useState } from "react";
const UNDO = 'UNDO'
const REDO = 'REDO'
const SET = 'SET'
const RESET = 'RESET'
type State<T> = {
past: T[];
present: T;
future: T[];
}
type Action<T> = { newPresent?: T, type: typeof UNDO | typeof REDO | typeof SET | typeof RESET }
const undoReducer = <T>(state: State<T>, action: Action<T>) => {
const { past, present, future } = state
const { newPresent, type } = action
switch(type) {
case UNDO: {
if (past.length === 0) return state;
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future],
};
}
case REDO: {
if (future.length === 0) return state;
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture,
};
}
case SET: {
if (newPresent === present) {
return state;
}
return {
past: [...past, present],
present: newPresent,
future: [],
};
}
case RESET: {
return {
past: [],
present: newPresent,
future: [],
}
}
default:
return state
}
}
export const useUndo = <T>(initialPresent: T) => {
const [state, dispatch] = useReducer(undoReducer, {
past: [],
present: initialPresent,
future: [],
} as State<T>)
const canUndo = state.past.length !== 0;
const canRedo = state.future.length !== 0;
const undo = useCallback(() => dispatch({ type: UNDO }), []);
const redo = useCallback(() => dispatch({ type: REDO }), []);
const set = useCallback((newPresent: T) => dispatch({newPresent, type: SET}), []);
const reset = useCallback((newPresent: T) => dispatch({newPresent, type: RESET}), []);
return [state, { redo, undo, set, reset, canRedo, canUndo }] as const;
};
统一状态管理后 虽然代码量多了,但是经过多重封装,层次更加清晰
可以发现 useReducer
相对 useState
适合定义多个相互影响的状态量
鉴于 useReducer
针对复杂的state关系和更新的前后依赖的优势,因此 useAsync
非常适合使用 useReducer
来重构
接下来使用 useReducer
改造一下 与 use-undo
结构类似的 use-async
(src\utils\use-async.ts
):
js
...
const useSafeDispatch = <T>(dispatch: (...args: T[]) => void) => {
const mountedRef = useMountedRef()
return useCallback((...args: T[]) => (mountedRef.current ? dispatch(...args) : void 0), [dispatch, mountedRef])
}
export const useAsync = <D>(...) => {
const config = { ...defaultConfig, ...initialConfig };
const [state, dispatch] = useReducer((state: State<D>, action: Partial<State<D>>) => ({...state, ...action}), {
...defaultInitialState,
...initialState,
});
const safeDispatch = useSafeDispatch(dispatch);
const [rerun, setRerun] = useState(() => () => {});
const setData = useCallback(
(data: D) =>
safeDispatch(...),
[safeDispatch]
);
const setError = useCallback(
(error: Error) =>
safeDispatch(...),
[safeDispatch]
);
// run 来触发异步请求
const run = useCallback(
(...) => {
...
safeDispatch({ stat: "loading" });
return promise
.then((data) => {
setData(data);
return data;
})
.catch(...);
},
[config.throwOnError, safeDispatch, setData, setError]
);
...
};
部分引用笔记还在草稿阶段,敬请期待。。。