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 注意点:
hooks 要放在最外层,层级要求最高.
不要在循环,条件,嵌套中使用 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 ) => { const oldTitle = React .useRef (document .title ).current () useEffect (()=> { document .title = title },[]) useEffect (()=> { return () => { if (!keepOnUnmount) { document .title = oldTitle } },[keepOnUnmount,oldTitle]) }
闭包陷阱 “闭包陷阱” 最大的问题就是在函数数内无法获取的最新的 state 的值. 函数式组件每次 render 都会生产一个新的 log 函数,这个新的 log 函数会产生一个在当前这个阶段 value 值的闭包。 log 方法内的 value 和点击触发时的 value 相同,value 的后续变化对 log 内的 value 不造成影响. 如何解决:
使用 useRef
原理: useRef 每次 render 时都会返回同一个引用类型的对象.设置值和读取值都在这个对象上处理.
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。 使用场景:
它不是状态,也就是说,不是用 useState 定义的(redux 中的状态实际上也是用 useState 定义的)
它不是基本类型
它会被放在 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 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 );AuthContext .displayName = 'AuthContext' const AuthProvider = ({children}:{children: ReactNode} ) => { return (<AuthContext.Provider children ={children} value ={/*各种方法的变量*/} /> ) } const useAuth = ( ) => { const context = useContext (AuthContext ) if (!context){ throw new Error ('useAuth必须在AuthProvider中使用' ) } return context } export const AppProvider = ({ children }: { children : ReactNode })) => { return ( <QueryClientProvider client ={new QueryClient ()}> <AuthProvider > {children}</AuthProvider > </QueryClientProvider > ) } <AppProviders > <DevTools /> <App /> </AppProviders >
redux react-query 这是一个适用于 React Hooks 的请求库,这个库将帮助你获取、同步、更新和缓存你的远程数据, 提供两个简单的 hooks,就能完成增删改查等操作,React-Query 使用声明式管理服务端状态,可以使用零配置开箱即用地处理缓存,后台更新和陈旧数据。
React-Query 封装了完整的请求中间状态(isLoading
、isError
…)。 不仅如此,React-Query 还为我们做了如下工作: 多个组件请求同一个 query 时只发出一个请求 缓存数据失效/更新策略(判断缓存合适失效,失效后自动请求数据) 对失效数据垃圾清理 数据的 CRUD 由 2 个 hook 处理:useQuery
处理数据的查useMutation
处理数据的增/删/改
乐观更新 更新操作后,在数据返回前可以先乐观的更新数据,但是有的情况下会有失败的可能.这时候再选择回滚更新.useMutation
的 onMutate
回调允许返回一个特定值,该值稍后将作为最后一个参数传递给 onError
和 onSettled
处理
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, { onMutate : async (newTodo) => { await queryClient.cancelQueries (["todos" ]); const previousTodos = queryClient.getQueryData (["todos" ]); queryClient.setQueryData (["todos" ], (old ) => [...old, newTodo]); return { previousTodos }; }, 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, }); const setData = (data: D ) => setState ({ data, error : null , stat : "success" , }); const setError = (error: Error ) => setState ({ error, data : null , stat : "error" , }); const run = (promise: Promise <D> ) => { if (!promise || !promise.then ) { throw new Error ("请传入Promise类型数据" ); } setState ({ ...state, stat : "loading" }); return promise .then ((data ) => { setData (data); return data; }) .catch ((error ) => { setError (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,而不是整个组件崩溃. 无法捕获:
事件处理
异步代码 (例如 setTimeout 或 requestAnimationFrame 回调函数)
服务端渲染
错误边界自身抛出来的错误 (而不是其子组件)
如果一个类组件定义了生命周期方法中的任何一个(或两个)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 (() => keys.reduce ((prev, key ) => { return { ...prev, [key]:searchParams.get ('key' ) || '' } },{} as {[key in K]: string }) ,[keys,searchParams]), (params: Partial<[key in K]: unknown > ) => setSearchParams (params) ] as const }
Shop 项目 使用 reduxjs/toolkit 中的 redux. reducer 采用切片的方法.
1 2 3 4 5 6 7 8 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 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 包裹