「✍ React 的 balabala」

190 阅读3分钟

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的执行顺序是:

  1. 非常耗时的同步操作完成
  2. microtask done 微任务
  3. count change => 20
  4. macrotask 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! 抽离了请求时的样板代码,类型提示也没有丢失!