项目问题集合

Jira

参数解构

用于简化浅拷贝操作

1
onChange={e => setParam(...param, e.target.value)}

fetch 方法使用

fetch 方法返回的是异步的可以使用 async/await

1
2
3
4
5
fetch("").then(async (res) => {
if (res.ok) {
setList(await res.json());
}
});

避免修改原值

对于修改某个值,可以先将该值拷贝,再修改拷贝值,将修改后的拷贝值抛出,避免修改原值.

1
2
3
4
5
removeIndex: (index) => {
const copy = [...value];
copy.splice(index, 1);
setValue(copy);
};

自定义 Hook

注意点:

  1. hooks 要放在最外层,层级要求最高.
  2. 不要在循环,条件,嵌套中使用 Hook,确保它在最顶层.

只在 React 函数中调用 Hook,不要在 js 函数中调用.

挂载时 Hook

1
2
3
4
5
const useMount = (callback) => {
useEffect(() => {
callback();
}, []);
};

防抖 Hook

1
2
3
4
5
6
7
8
9
10
11
12
// 参数一,需要防抖的值,可以是任意类型
const useDebounce = <V>(value: V, delay?: number): any => {
const [debounceValue, setDebounceValue] = useState();

useEffect(() => {
//挂载时设置定时器
const timeout = setTimeout(() => setDebounceValue(value), (delay = 1000));
//卸载时,清除定时器
return () => cleatTimeout(timeout);
//设置的依赖用于变化后重新执行
}, [value, delay]);
};

Hooks 中的闭包陷阱

以设置网站标题为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useDocument = (title:string, keepOnUnmount?:boolean=true) => {
// 拿到文档当前的title
const oldTitle = React.useRef(document.title).current()
// 挂载时,将传入title赋值
useEffect(()=>{
document.title = title
},[])
// 卸载时,判断是否有保持传入title的参数
useEffect(()=>{
//这里闭包里保存的oldTitle没有变化
return () => {
if(!keepOnUnmount) {
document.title = oldTitle
}
},[keepOnUnmount,oldTitle])
}

闭包陷阱

“闭包陷阱” 最大的问题就是在函数数内无法获取的最新的 state 的值.
函数式组件每次 render 都会生产一个新的 log 函数,这个新的 log 函数会产生一个在当前这个阶段 value 值的闭包。
log 方法内的 value 和点击触发时的 value 相同,value 的后续变化对 log 内的 value 不造成影响.
如何解决:

  1. 使用 useRef

原理: useRef 每次 render 时都会返回同一个引用类型的对象.设置值和读取值都在这个对象上处理.

  1. useState 更新值时传入回调函数

**setValue**(value => value + 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const FunctionComponent = () => {
const [value, setValue] = useState(1);
const log = () => {
setTimeout(() => {
alert(value);
}, 1000);
};
return (
<div>
<p>FunctionComponent</p>
<div>value: {value}</div>
<button onClick={() => setValue(value + 1)}>add</button>
<br />
<button onClick={log}>alert</button>
</div>
);
};

副作用

副作用(Side Effect)是指函数或者表达式的行为依赖于外部世界。

useMemo 和 useCallback

而 useCallback 就是一个特殊版本的 useMemo,专门来处理函数的
案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "./styles.css";
import React from "react";

export default function App() {
const [count, setCount] = React.useState(0); // 加了这一行
const value = { name: 1 };

React.useEffect(() => {
setCount(Math.random()); // 加了这一行
("render");
}, [value]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Edit to see some magic happen!</h2>
</div>
);
}

这里循环的原因是:组件渲染 → useEffect 执行 → setCount 触发循环 → 组件渲染 → useEffect 执行 → setCount 触发循环…
注意是组件渲染之后,刷新的是 value,和上一个不是同一个 value.那就需要给它套上 useMemo.

1
2
3
const value = React.useMemo(() => {
return { name: 1 };
}, []);

useMemo 的意思就是:不要每次渲染都重新定义,而是我让你重新定义的时候再重新定义(第二个参数,依赖列表)。大家看到这里的依赖列表是空的,是因为 useMemo 里的回调函数确实没用到啥变量,如果有变量的话大家的 IDE 就会提醒加上依赖了。
这就是使用 useMemo 的原理,useMemo 适用于所有类型的值,加入这个值恰好是函数,那么用 useCallback 也可以。也就是说,useCallback 是一种特殊的 useMemo。
使用场景:

  1. 它不是状态,也就是说,不是用 useState 定义的(redux 中的状态实际上也是用 useState 定义的)
  2. 它不是基本类型
  3. 它会被放在 useEffect 的依赖列表里 || 自定义 hook 的返回值

自定义 hook 的返回值也成立是因为,你不知道自定义 hook 的返回值将会被用在哪里,它可能会被用在依赖也可能不会,所以干脆都加上;而像上面那个在组件中定义的 value,你就可以见机行事了

自定义 Hook

自定义 Hook 是目前最好的重用 React 逻辑的方法,它和普通的函数很像很像,自定义 Hook 的特殊之处在于,它是有状态的,它返回的也是状态。所以在什么时候我们应该用到自定义 Hook?那就是,我们想要抽象出处理状态的逻辑的时候

状态管理

大概可以分为三种场景
简单场景:使用状态提升,组合组件
使用缓存管理:react-query
使用客户端状态管理:redux, context, url

关于全局使用的状态操作

方法一: 可以使用 redux 设置一个 state,设置对应的 reducer,
一个同步设置状态,一个异步 thunk.
方法二: 使用缓存,react-query.

useContext

给整个 app 设置登录注册用户信息相关的 context.将 app 包裹,就可以随时验证登录状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//1.创建Context
interface IContext {
user: User | null;
login: (form:AuthForm) => Promise<void>;
register: (form:AuthForm) => Promise<void>;
logout: () => Promise<void>;
}
interface AuthForm {
username: string;
password: string;
}
const AuthContext = createContext<IContext| undefined>(undefined);
// 2.设置Context的名字
AuthContext.displayName = 'AuthContext'
// 3. 设置provider包裹整个app组件
const AuthProvider = ({children}:{children: ReactNode}) => {
// 各种在登录或者要验证用户信息的方法
// 设置报错反馈
return (<AuthContext.Provider children={children} value={/*各种方法的变量*/} />)
}
// 4. 使用自定义Hook设置调用方法
const useAuth = () => {
const context = useContext(AuthContext)
if(!context){
throw new Error('useAuth必须在AuthProvider中使用')
}
return context
}
// 5. 将AuthProvider和父级的Provider合并
export const AppProvider = ({ children }: { children: ReactNode })) => {
return (
<QueryClientProvider client={new QueryClient()}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
)
}
// 6. 在index.ts文件将整个app组件包裹
<AppProviders>
<DevTools />
<App />
</AppProviders>

redux

react-query

这是一个适用于 React Hooks 的请求库,这个库将帮助你获取、同步、更新和缓存你的远程数据, 提供两个简单的 hooks,就能完成增删改查等操作,React-Query 使用声明式管理服务端状态,可以使用零配置开箱即用地处理缓存,后台更新和陈旧数据。

React-Query 封装了完整的请求中间状态(isLoadingisError…)。
不仅如此,React-Query 还为我们做了如下工作:
多个组件请求同一个 query 时只发出一个请求
缓存数据失效/更新策略(判断缓存合适失效,失效后自动请求数据)
对失效数据垃圾清理
数据的 CRUD 由 2 个 hook 处理:
useQuery处理数据的查
useMutation处理数据的增/删/改

乐观更新

更新操作后,在数据返回前可以先乐观的更新数据,但是有的情况下会有失败的可能.这时候再选择回滚更新.
useMutationonMutate 回调允许返回一个特定值,该值稍后将作为最后一个参数传递给 onErroronSettled 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const queryClient = useQueryClient();

useMutation(updateTodo, {
// 当 mutate 调用时
onMutate: async (newTodo) => {
// 撤销相关的查询(这样它们就不会覆盖我们的乐观更新)
await queryClient.cancelQueries(["todos"]);

// 保存前一次状态的快照
const previousTodos = queryClient.getQueryData(["todos"]);

// 执行"乐观"更新
queryClient.setQueryData(["todos"], (old) => [...old, newTodo]);

// 返回具有快照值的上下文对象
return { previousTodos };
},
// 如果修改失败,则使用 onMutate 返回的上下文进行回滚
onError: (err, newTodo, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
// 总是在错误或成功之后重新获取:
onSettled: () => {
queryClient.invalidateQueries("todos");
},
});

项目案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { QueryKey, useQueryClient } from "react-query";
import { Task } from "types/task";
import { reorder } from "./reorder";

export const useConfig = (
queryKey: QueryKey,
callback: (target: any, old?: any[]) => any[]
) => {
const queryClient = useQueryClient();
return {
// 第二个参数即在成功获取数据后刷新数据
onSuccess: () => queryClient.invalidateQueries(queryKey),
// 配置乐观更新
onMutate: async (target: any) => {
const previousItems = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, (old?: any[]) => {
return callback(target, old);
});
return {
previousItems,
};
},
// 如果出错就将原数据回滚
onError: (error: any, newItem: any, context: any) => {
queryClient.setQueryData(queryKey, context.previousItems);
},
};
};

fetch 与 axios 区别

使用 fetch 封装,注意和 axios 的区别,无法捕获非网络异常导致的 error.必须手动 Promise.reject()抛出.

1
2
3
4
5
6
const data = await res.json();
if (res.ok) {
return data;
} else {
return Promise.reject(data);
}

无感登录

在挂载时,设置方法.获取 token,更新 user 信息.没有 token 后端返回 401 就跳转到登录页.

1
2
3
4
5
6
7
8
9
const bootstrapUser = async () => {
let user = null;
const token = auth.getToken();
if (!token) {
const data = await http("me", { token });
user = data.user;
}
return user;
};

乐观更新

自定义 Hook 异步操作

自带 loading,错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { useState } from "react";

interface State<D> {
error: Error | null;
data: D | null;
stat: "idle" | "loading" | "error" | "success";
}

const defaultInitialState: State = {
stat: "idle",
data: null,
error: null,
};

export const useAsync = <D>(initialState?: State<D>) => {
const [state, setState] = useState<State<D>>({
...defaultInitialState,
initialState,
});

//设置data说明状态成功
const setData = (data: D) =>
setState({
data,
error: null,
stat: "success",
});
//设置data说明状态失败
const setError = (error: Error) =>
setState({
error,
data: null,
stat: "error",
});
// 接收异步
const run = (promise: Promise<D>) => {
// 如果不是promise类型报错
if (!promise || !promise.then) {
throw new Error("请传入Promise类型数据");
}
// 如果是,刚开始是loading状态
setState({ ...state, stat: "loading" });
// 最后返回promise
return promise
.then((data) => {
setData(data);
return data;
})
.catch((error) => {
setError(error);
// 注意catch会捕获error,不主动抛出就不能继续往下传递
return Promise.reject(error);
});
};
// 将所有信息暴露
return {
isIdle: state.stat === "idle",
isLoading: state.stat === "loading",
isError: state.stat === "error",
isSuccess: state.stat === "success",
run,
setData,
setError,
...state,
};
};

错误边界

捕获错误显示备用 UI,而不是整个组件崩溃.
无法捕获:

  1. 事件处理
  2. 异步代码 (例如 setTimeout 或 requestAnimationFrame 回调函数)
  3. 服务端渲染
  4. 错误边界自身抛出来的错误 (而不是其子组件)

如果一个类组件定义了生命周期方法中的任何一个(或两个)static getDerivedStateFromError()componentDidCatch(),那么它就成了一个错误边界。
使用static getDerivedStateFromError()在抛出错误后渲染回退 UI。 使用 componentDidCatch() 来记录错误信息。

实现方法

需要使用类组件声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";

type FallBackRender = (props: { error: Error | null }) => React.ReactElement;

export class ErrorBoundary extends React.Component<
React.PropsWithChildren<{ fallbackRender: FallBackRender }>,
{ error: Error | null }
> {
state = { error: null };
// 当子组件抛出异常,这里会接收到并且调用
static getDerivedStateFromError(error: Error) {
return { error };
}

render() {
const { error } = this.state;
const { fallbackRender, children } = this.props;
if (error) {
return fallbackRender({ error });
}
return children;
}
}

然后包裹整个组件树

类型守卫

类型守卫(当符合条件时,value 就是 Error 类型)

1
const isError = (value: any): value is Error => value?.message;

React Router

在要使用的页面构建 Router 的 Context.

路由参数 Hook

由此设置依据 url 返回参数进行数据传递.
作用:返回页面 URL 中指定键的参数值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useUrlQueryParam = <K extends string>(keys: K[]) => {
const [searchParams, setSearchParams] = useSearchParams()

return [
//使用useMemo优化这个方法
useMemo(() =>
keys.reduce((prev, key) => {
return {
...prev, [key]:searchParams.get('key') || ''
}
},{} as {[key in K]: string})
,[keys,searchParams]),
// 返回setSearchParams,约束入参
(params: Partial<[key in K]: unknown>) =>setSearchParams(params)
] as const
}

Shop 项目

使用 reduxjs/toolkit 中的 redux.
reducer 采用切片的方法.

1
2
3
4
5
6
7
8
// store/index.ts
// 设置根reducer对象
export const rootReducer = {
auth: authSlice.reducer,
};
export const store = configureStore({
reducer: rootReducer,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// auth.slice.ts
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser(state,action) {
state.user = action.payload
}
}
})

const {setUser} = authSlice.actions;

export const selectUser = (state:RootState) => state.auth.user
export const login = (form:AuthForm) => (dispatch:AppDispatch) =>auth.login(form).then(user => dispatch(setUser(user))

记得使用 Provider 将 Context 包裹