react 自定义异步 hook 踩坑

289 阅读2分钟

vue 封装

重度 Vue 使用者最近一直在写 React 代码,很容易用 Vue 的出发写代码,然后跳到坑里。比如 3 个页面都是用了某个请求,Vue 开发很容易写出下面的代码:

import {ref} from 'vue';
const mockPromise = () => new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('mock error')
  }, 1000)
})
export const useFetch = () => {
  const isLoading = ref(false)
  const isError = ref(false)
  const data = ref('')
  const fetchData = async () => {
    try {
      isLoading.value = true
      const res = await mockPromise();
      data.value = res
      isError.value = false
    } catch {
      isError.value = true
    } finally {
      isLoading.value = false
    }
  };
  return { isLoading, fetchData, isError, data };
};

然后使用封装的 hook:

<script setup>
import { ref } from 'vue'
import {useFetch} from './useFetch'
const {isLoading, fetchData, isError, data } = useFetch()
const handleClick = async() => {
  await fetchData()
  if (isError.value) {
    alert('请求出错')
  }
}
</script>
<template>
  <button @click="handleClick">请求</button>
</template>

image.png

react 封装

按照 vue 的逻辑,我很快写出了 react 封装的 hook,还洋洋自得,自己封装的就是好用,🤣 结果出 bug 了,我们先看下错误代码 ❌:

// ❌
import { useState} from 'react';
const mockPromise = () => new Promise((resolve,reject) => {
  setTimeout(() => {
    reject('mock error')
  }, 1000)
})
 
export const useFetch = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [data, setData] = useState(null);

  const fetchData = async () => {
    try {
      setIsLoading(true);
      const res = await mockPromise();
      setData(res);
      setIsError(false);
    } catch (error) {
      setIsError(true);
    } finally {
      setIsLoading(false);
    }
  };
  return { isLoading, fetchData, isError, data };
};

使用:


  const { isLoading, fetchData, isError, data } = useFetch();
  const handleFetch = async() => {
    await fetchData();
    if(isError) {
      alert('error')
    }
  }
  return (
    <button onClick={handleFetch}>发请求</button>
  )

为什么会这样呢?感觉上是对的,结果不是。问题出在了请求完数据,我觉得 isError 应该会自动变成 true ,但是 React 更新数据是异步的,也就是:

// count 是 0
setCount(count+1)
console.log(count) // 输出还是 0

对于 React 来说每一次执行就是一次函数的执行,可以理解成第二次函数执行的结果并不会影响第一次函数的执行。

解决方案

使用 useRef,useRef 每次都会返回相同的引用,拿到最新的值。

import { useState, useRef } from 'react';

const mockPromise = () => new Promise((resolve,reject) => {
  setTimeout(() => {
    reject('mock error')
  }, 1000)
})
 
export const useFetch = () => {
  const [isLoading, setIsLoading] = useState(false);
  const isError = useRef(false);
  const [data, setData] = useState(null);

  const fetchData = async () => {
    try {
      setIsLoading(true);
      const res = await mockPromise();
      setData(res);
      isError.current = false;
    } catch (error) {
      isError.current = true;
    } finally {
      setIsLoading(false);
    }
  };
  return { isLoading, fetchData, isError, data };
};

使用:


  const { isLoading, fetchData, isError, data } = useFetch();
  const handleFetch = async() => {
    await fetchData();
    if(isError.current) {
      alert('error')
    }
  }
  return (
    <button onClick={handleFetch}>发请求</button>
  )