用useReducer代替useState !
有时候,我们想在useEffect(()=>{},[])中去做一些初始化的工作,如:
const [page,setPage] = useState(0)
const [count,setCount] = useState(10)
useEffect(()=>{
setCount(c => c + page)
},[])
这时eslint就开始表演了,它告诉你useEffect中使用了page,所以你需要在依赖项中添加page
fine,那就加上吧
const [page,setPage] = useState(0)
const [count,setCount] = useState(10)
useEffect(()=>{
setCount(c => c + page)
},[page])
没有了,没有报错了。但我明明只是想在初始化的时候调用,鬼知道page什么时候会被改变 ??
useReducer出场!
const initState = {
count: 0,
page: 10
}
const [{ count, page }, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'initCount':
return { ...state, count: state.count + state.page }
default:
return state
}
}, initState)
useEffect(() => {
dispatch({
type: 'initCount'
})
}, [dispatch])
useEffect只需要依赖dispatch, 而不需要依赖任何state,而useReducer并不会随着组件更新而重新创建,因此dispatch也不会重新创建,那么这个useEffect也就理所当然的仅在初始化时执行了
此外,useReducer并不影响我们将useEffect作为监听器使用
useEffect(() => {
alert(count)
// do sth ..
}, [count])
setState 是同步还是异步 ?
import React, { useEffect, useState } from "react";
const App = (props) => {
// 查看render次数
useEffect(() => {
console.log("render" + Math.random());
});
const [count, setCount] = useState(0);
useEffect(() => {
for (let i = 0; i < 20; i++) {
setCount((c) => c + 1);
}
// 耗时的同步操作
for (let i = 0; i < 2000000; i++) {
JSON.parse(JSON.stringify({ name: "jenson" }));
}
alert("非常耗时的同步操作完成");
// 异步 - 微任务
Promise.resolve().then(() => alert("micratask done"));
// 异步 - 宏任务
setTimeout(() => {
JSON.parse(JSON.stringify({ name: "jenson" }));
alert("macrotask done" + count); // count = 0 下面会说到
}, 0);
}, []);
useEffect(() => {
alert("count change=>" + count);
}, [count]);
return <h1>{count}</h1>
};
在上面这个demo中,alert的执行顺序是:
非常耗时的同步操作完成microtask done微任务count change => 20macrotask done宏任务
由此可见,setState 就像eventloop中的render一样,在一轮eventloop结束后执行,然后执行下一轮loop
( 复习一下eventloop:执行同步代码 => 执行一个宏任务 => 清空所有微任务 => render.. => 下一轮 )
因此,如果需要在setState后执行一些耗时任务,可以放在macrotask中,比如setTimeout。
注意! 如果你在function component中做这件事,是会失败的,因为FC中存在captaure的概念,它会捕获当前帧的数据,你在setTimeout中会发现count还是0
所以在FC中请使用useEffect来监听state并执行想要的代码
useEffect(() => {
for (let i = 0; i < 20; i++) {
setCount((c) => c + 1);
}
},[])
useEffect(() => {
// do sth with latest state count
},[count])
而在class组件中不会出现上述情况,setTimeout中可以获取到最新的count = 20
class App extends React.Component {
state = {
count: 0,
};
componentDidMount() {
for (let i = 0; i < 20; i++) {
this.setState((state) => ({ count: state.count + 1 }));
}
for (let i = 0; i < 10; i++) {
JSON.parse(JSON.stringify({ name: "jenson" }));
}
alert('同步完成' + this.state.count) // count = 0
// 异步操作
setTimeout(() => {
JSON.parse(JSON.stringify({ name: "jenson" }));
alert("macrotask done" + this.state.count); // count = 20
}, 0);
}
render() {
console.log("render" + Math.random());
return <h1>{this.state.count}</h1>;
}
}
怎么给React FC写类型?
interface IProps {
sn: string
}
const SnPermission: React.FC<IProps> = ({ sn, children }) => {
const { user_powers } = useSelector((state: AppState) => state.global)
const super_admin = lStorage.getItem('super_admin')
// 查看权限
if (user_powers.includes(sn)) {
return <>{ children }</>
}
return null
}
Props 中, sn是使用者传递,children则是props自身的属性,如果这样写会发现找不到children了,因此我们需要手动定义一下阻止原有的属性类型被覆盖
type IProps = { sn: string } & RouteChildrenProps & PropsWithChildren<null>
const SnPermission: React.FC<IProps> = ({ sn, children }) => {
const { user_powers } = useSelector((state: AppState) => state.global)
const super_admin = lStorage.getItem('super_admin')
// 查看权限
if (user_powers.includes(sn)) {
return <>{ children }</>
}
return null
}
其中
RouteChildrenProps
export interface RouteChildrenProps<Params extends { [K in keyof Params]?: string } = {}, S = H.LocationState> {
history: H.History;
location: H.Location<S>;
match: match<Params> | null;
}
PropsWithChildren
type PropsWithChildren<P> = P & { children?: ReactNode };
封装一个useFetch hook
厌倦了给每个请求都做异常捕获,做loading和error提示,就用一个useFetch来封装吧
import { useCallback, useState } from 'react'
import { Toast } from 'antd-mobile'
// 响应数据
type IRespData = {
result: number | string
res_info: string
data: any
}
// 请求函数
type IReqFun<T> = (params?: T) => Promise<IRespData>
type IStatus = 'loading' | 'success' | 'fail' | 'standby'
type IFetch = <T>(params: { excuteFn: IReqFun<T> }) => {
data: unknown,
status: IStatus,
excute: (params: T) => void
}
/**
*
* @param excuteFn
* @returns
*/
const useFetch: IFetch = (props) => {
const { excuteFn } = props
const [data, setData] = useState({})
const [status, setStatus] = useState<IStatus>('standby')
const excute = useCallback((params) => {
setStatus('loading')
excuteFn(params).then(res => {
const { result, res_info, result_rows } = res
if (result !== 0) {
Toast.success(`[${ result }]${ res_info }`)
setStatus('fail')
return
}
setStatus('success')
setData(result_rows)
}).catch(() => {
setStatus('fail')
})
}, [excuteFn])
return {
excute, status, data
}
}
export default useFetch
使用时我们需要传入一个Promise
const { excute, status, data } = useFetch({
excuteFn: queryData
})
useEffect(() => {
excute({
order_id,
})
}, [excute, order_id])
queryData
type queryDataType = {
order_id: string
}
export const queryData: IReqFun<queryDataType> = (params) =>
Http.post('/api/data/query', params)
done! 抽离了请求时的样板代码,类型提示也没有丢失!