likes
comments
collection
share

React 开发总结

作者站长头像
站长
· 阅读数 38

使用 React 开发的一些总结

渲染

  • 同时改变 props & state,会触发几次渲染? 比如:在 handleClick 中调用函数改变 props 同时 setState,只会触发一次渲染
  • react useEffect, useCallback, useMemo 的 dependencies -> [a]: 表示在第 1 次和 a 改变的时候会去执行;
  • 如果依赖项是对象,两个对象必须是同一个对象 useEffect 才会跳过执行 effect。所以,即使内容完全相同,内存地址不同的话,useEffect 还是会执行 effect
  • 如果一个函数作为 props 传给子组件A,那么它最好使用 useCallback,否则会因为父组件其他 props/state(子组件A未使用到的)的更新导致该函数更新,进而导致子组件A重复渲染;
  • 如果一个变量作为 props 传给子组件A,也会因为父组件其他 props/state(子组件A未使用到的)的更新导致子组件A重复渲染,可以使用 useMemo(props),更推荐 React.memo(A),这样写法简单,不用每个 props 都 useMemo 一遍;
  • .then, setTimeout 回调中每次 setState 都会触发一次渲染。
  • react 事件回调函数中(比如:handleClick)多次 setState 只会触发一次渲染。
  • const [value, setValue] = useState(props.value) 这种写法在 props.value 变化时不会更新 value 的值。

依赖

React 官方推荐的依赖插件会自动加上依赖,那么自动加上的是啥呢?

  • state: 不会加上 setState, 例如:const [visible, setVisible] = useState(false), visible 是依赖,setVisible 不是
  • props: 变量 props & 函数 props
  • const memoVariable = useMemo()
  • const handleClick = useCallback()
  • 内部变量:在内部定义的没有用 useMemo 包起来的变量
  • 内部函数:在内部定义的没有用 useCallback 包起来的函数

哪些不会被当成 dependencies:

  • 函数入参
  • 引用的第三方

实例:

下面的 getBrandList 依赖是插件自动添加的

const { brandId: qcBrandId, qcOriginalRegion } = props.qcBrand;

const [loading, setLoading] = useState<boolean>(false);
const [searchType, setSearchType] = useState<SearchType>(SearchType.ID);
const debounceSearch = useDebounce(search, 500);
const [page, setPage] = useState<number>(DEFAULT_PAGE);

const region = getCountry();
const regionList = useMemo(
  () =>
    isLocal
      ? [region, WORLDWIDE]
      : qcOriginalRegion
      ? [qcOriginalRegion, WORLDWIDE]
     : [WORLDWIDE],
   [isLocal, qcOriginalRegion, region],
 );

const getBrandList = useCallback(async () => {
    try {
      if (loading || !qcBrandId) return;

      setLoading(true);
      const searchParams: globalBrand.ISearchBrandWithBlacklistInfoRequest = {
        regionList,
        statusList: [BRAND_NORMAL],
        offset: page * 10,
        limit: 10,
        orderBy: 'ctime desc',
        subBrandId: qcBrandId,
      };

      const search = debounceSearch;
      if (search) {
        if (searchType === SearchType.ID) {
          const brandId = parseInt(search);
          if (!isBrandId(brandId)) {
            setLoading(false);
            return;
          }
          searchParams.brandId = brandId;
        } else {
          searchParams.brandName = search;
        }
      }

      const resp =
        (await searchBrandWithBlacklistInfo(searchParams))?.brandList || [];
      const brandList = resp.map(
        ({ blacklistedCatWithKeywords, brand }) =>
          ({
            ...brand,
            blacklistedCatWithKeywords,
          } as IBrandWithDuplicateInfo),
      );
      setBrandList((brands) =>
        page === DEFAULT_PAGE ? brandList : [...brands, ...brandList],
      );

      onAfterResponse && onAfterResponse();
      setLoading(false);
    } catch (error) {
      onAfterResponse && onAfterResponse();
      setLoading(false);
    }
  }, [
    loading,
    qcBrandId, 
    regionList,
    page,
    debounceSearch,
    onAfterResponse,
    searchType,
  ]);

插件帮我们加的依赖是否都是必须的呢?多数情况是需要的,但有时候会造成重复请求/重复渲染。

开发中一个常见的场景:先 loading, 发送请求,拿到返回数据后,loading 消失。下面的写法如果使用插件自动添加依赖的话就会造成重复请求。

const [search, setSearch] = useState('');
const [loading, setLoading] = useState(false)
const [data, setData] = useState();

const getData = useCallback(aync () => {
  if (loading) return;

  setLoading(true)
  const res = await getDataFromServer({
     search,
  })
  setData(res)
  setLoading(false)
}, [search, loading]) // 插件会自动加上 search, loading

useEffect(() => {
  getData()
}, [getData]) // 插件会自动加上 getData

执行过程:

第 4 次渲染和第 1 次很像:调用 getData, setLoading(false)。然后继续触发第 2、3 次渲染。整个渲染过程其实就是[1,2,3]的循环,因此造成重复请求。 React 开发总结

上面有两种改法:

  • 方式一:useEffect 的依赖改成 search
const getData = useCallback(() => {}, [search, loading])
useEffect(() => {
  getData()
}, [search])
  • 方式二:useCallback 的依赖改成 search
const getData = useCallback(() => {}, [search])
useEffect(() => {
  getData()
}, [getData])

哪种方式更好呢?

之所以总结是因为我开发时多次遇到重复请求/重复渲染的问题,我希望在一开始写代码的时候就能避免写成重复请求/重复渲染的代码。那有没有一种规范,如果我们遵守这个规范,就能达到目的,即:尽量在一开始写代码的时候就正确添加依赖,不额外渲染,不重复请求

探索添加依赖的一种实践规范:

  • 不建议一上来就使用依赖插件。很多时候,我们不会仔细看自动添加了哪些依赖,我接手的第一个 react 项目开启了保存自动添加依赖的功能,当时就是无脑添加,出了问题再去改;后面新的项目没有开启这个功能,手动添加就变得不知所措。依赖插件会造成惰性心理。
  • useCallback / useMemo 加上所有依赖。useCallback / useMemo 闭包了 state / props 等,当它们变化时如果 useCallback / useMemo 没有更新,就会导致执行的时候取的是 state / props 旧值,所以加上所有依赖。由于依赖很多,自己去添加可能会遗漏,这时可以利用插件加上所有。
  • useEffect 依赖不使用插件添加。添加依赖的核心是依赖的变化是否需要 useEffect 重复执行,如果需要的话,那它是一个依赖,如果不需要,即使插件帮我们自动加上了,也应该去掉,否则可能造成死循环,陷入重复渲染!

基于上面的规范,我觉得方式二比较好。

场景

示例一

我在开发中经常遇到的一个场景是:有一个组件 A,可接收一个外部值作为初始值,组件 A 可修改该状态。这种场景有多少种实现方式呢?

方式一:组件 A 内部维护一个 state,状态变更时使用 state 保存最新状态,同时调用 props.onChange 将最新状态暴露给父组件;使用 useEffect 设置初始值,并且 props.value 变更时去更新 state。

const [value, setValue] = useState()

const handleClick = useCallback((val) => {
  setValue(val)
  props.onChange && props.onChange(val)
})

useEffect(() => {
  setValue(props.value)
}, [props.value])

方式二:其他同上,但使用 useEffect 设置初始值时,props.value 不作为依赖。

const [value, setValue] = useState()

const handleClick = useCallback((val) => {
  setValue(val)
  props.onChange && props.onChange(val)
})

useEffect(() => {
  setValue(props.value)
}, [])

方式一和方式二用哪种呢?

方式一是更通用的一种写法,如果父组件中的某个操作改变了 props.value,那么子组件也能及时更新。antd 的 radio/input/select 都是类似的写法。如果是实现一个很基础通用的组件,那么建议第一种。如果是业务组件,已经明确知道父组件不会有改变 props.value 的操作,那么可以用第二种。

方式三:使用方式一的时候,发现 handleClick 调用后 setValue 了两次同样的值(1次在 handleClick,1次在 useEffect),所以在方式一的基础上删除了 setValue(val),发现:哎,能用,还少了一次 setState,真棒!但其实这种写法有个问题:组件 A 此时维护的是个空壳状态!一旦父组件没有提供 onChange,组件 A 内部操作无法更新状态,所以,不要这样写

const [value, setValue] = useState()

const handleClick = useCallback((val) => {
  props.onChange && props.onChange(val)
})

useEffect(() => {
  setValue(props.value)
}, [props.value])

方式四:组件 A 内部没有维护一个 state,状态变更时直接调用 props.onChange 将最新值传给父组件;父组件更新状态(父组件维护 state)从而组件 A 重新渲染。

const handleClick = useCallback((val) => {
  props.onChange && props.onChange(val)
})
return (
  <div>{props.value}</div>
)

方式四适合纯展示的组件,内部无需维护状态。

示例二

如果组件 A 在需求初期需要父组件传对象的一个属性,那是将该属性作为 props 还是该对象作为 props 呢?

我一开始开发的时候会选择传属性,觉得传那么多数据干嘛?但好多次的项目经验都证明后续需要的属性会越来越多,还不如一开始传对象。这个场景其实还是要具体分析了,特别提出来提醒自己下,如果能在开发初期预见更好,省的后续改代码了,你们说是不?

---------------------------------这是一条分界线--------------------------------

懒了大半年了🥱...... 中秋快乐🎑!打工人们

转载自:https://juejin.cn/post/7142404503868801054
评论
请登录