发布于 2026-01-06 8 阅读
0

我保证这个钩子会让你的 1000 多行异步代码焕然一新。

我保证这个钩子会让你的 1000 多行异步代码焕然一新。

我可没有双关的意思! ;)

我从 Kent 的 Epic React Workshop 中学到的 hook代码useAsync()如下所示:

function useSafeDispatch(dispatch) {
  const mounted = React.useRef(false)

  React.useLayoutEffect(() => {
    mounted.current = true
    return () => (mounted.current = false)
  }, [])
  return React.useCallback(
    (...args) => (mounted.current ? dispatch(...args) : void 0),
    [dispatch],
  )
}

const defaultInitialState = {status: 'idle', data: null, error: null}

function useAsync(initialState) {
  const initialStateRef = React.useRef({
    ...defaultInitialState,
    ...initialState,
  })
  const [{status, data, error}, setState] = React.useReducer(
    (s, a) => ({...s, ...a}),
    initialStateRef.current,
  )

  const safeSetState = useSafeDispatch(setState)

  const setData = React.useCallback(
    data => safeSetState({data, status: 'resolved'}),
    [safeSetState],
  )
  const setError = React.useCallback(
    error => safeSetState({error, status: 'rejected'}),
    [safeSetState],
  )
  const reset = React.useCallback(
    () => safeSetState(initialStateRef.current),
    [safeSetState],
  )

  const run = React.useCallback(
    promise => {
      if (!promise || !promise.then) {
        throw new Error(
          `The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
        )
      }
      safeSetState({status: 'pending'})
      return promise.then(
        data => {
          setData(data)
          return data
        },
        error => {
          setError(error)
          return Promise.reject(error)
        },
      )
    },
    [safeSetState, setData, setError],
  )

  return {
    isIdle: status === 'idle',
    isLoading: status === 'pending',
    isError: status === 'rejected',
    isSuccess: status === 'resolved',

    setData,
    setError,
    error,
    status,
    data,
    run,
    reset,
  }
}

export {useAsync}
Enter fullscreen mode Exit fullscreen mode

我们将使用钩子重构BookInfo下面的组件,通过精简多行代码使其更加优雅和健壮。💣

import * as React from 'react'
import {
  fetchBook,
  BookInfoFallback,
  BookForm,
  BookDataView,
  ErrorFallback,
} from '../book'

function BookInfo({bookName}) {
  const [status, setStatus] = React.useState('idle')
  const [book, setBook] = React.useState(null)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    if (!bookName) {
      return
    }
    setStatus('pending')
    fetchBook(bookName).then(
      book => {
        setBook(book)
        setStatus('resolved')
      },
      error => {
        setError(error)
        setStatus('rejected')
      },
    )
  }, [bookName])

  if (status === 'idle') {
    return 'Submit a book'
  } else if (status === 'pending') {
    return <BookInfoFallback name={bookName} />
  } else if (status === 'rejected') {
    return <ErrorFallback error={error}/>
  } else if (status === 'resolved') {
    return <BookDataView book={book} />
  }

  throw new Error('This should be impossible')
}

function App() {
  const [bookName, setBookName] = React.useState('')

  function handleSubmit(newBookName) {
    setBookName(newBookName)
  }

  return (
    <div className="book-info-app">
      <BookForm bookName={bookName} onSubmit={handleSubmit} />
      <hr />
      <div className="book-info">
        <BookInfo bookName={bookName} />
      </div>
    </div>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode

我超级兴奋,我们开始吧!

超级兴奋的GIF

但在继续之前,让我们先达成共识。

  • fetchBook从 API 获取数据,并返回 Promise,如果成功则返回书籍数据,如果被拒绝则返回错误信息。

  • BookInfoFallback这是你的加载器组件,它接受 bookName 作为参数来显示漂亮的加载效果。

  • BookForm这是一个简单的表单组件,用于接收用户数据。

  • BookDataView是一个外观精美的组件,用于向用户显示图书数据。

  • ErrorFallback显示美观的错误信息界面。

这些组件的实现超出了这篇博客的范围,但它们只是一些常规的东西。

我们的代码到底在干什么?

它从用户那里获取书名,并将其传递给组件,BookInfo该组件在钩子中处理获取书数据的操作,并根据不同的条件设置状态,它还处理成功获取、失败获取和加载时useEffect的渲染。BookDataViewErrorFallbackBookInfoFallback

好吧,我可能触发了什么。

“光说不练假把式,拿出代码来”的时刻。

困惑的GIF

import * as React from 'react'
import {
  fetchBook,
  BookInfoFallback,
  BookForm,
  BookDataView,
  ErrorFallback,
} from '../book'
import useAsync from '../utils';

function BookInfo({bookName}) {
  /////////////// Focus from here /////////////////
  const {data: book, isIdle, isLoading, isError, error, run} = useAsync()

  React.useEffect(() => {
     if (!pokemonName) {
       return
     }
     run(fetchPokemon(pokemonName))
  }, [pokemonName, run])

  if (isIdle) {
    return 'Submit a book'
  } else if (isLoading) {
    return <BookInfoFallback name={bookName} />
  } else if (isError) {
    return <ErrorFallback error={error}/>
  } else if (isSuccess) {
    return <BookDataView book={book} />
  }
 //////////////// To here /////////////////

  throw new Error('This should be impossible')
}

function App() {
  const [bookName, setBookName] = React.useState('')

  function handleSubmit(newBookName) {
    setBookName(newBookName)
  }

  return (
    <div className="book-info-app">
      <BookForm bookName={bookName} onSubmit={handleSubmit} />
      <hr />
      <div className="book-info">
        <BookInfo bookName={bookName} />
      </div>
    </div>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode

哇,现在是不是很棒?这不仅使我们的代码更易读,而且通过在组件卸载时不调用 dispatch 来增强组件的健壮性,此外,我们还对 fetch 方法进行了记忆化,以便在 bookName 没有改变时节省网络调用。

但是,Harsh,我们是不是为了完成一些很常见的事情而编写了更多代码?

是的,我们确实在这样做,但通过编写这个钩子,我们可以使用异步代码重构整个项目中的多个组件,这样可以节省大量时间,减少交付的代码量,并显著提高信心。

这是钩子的第一部分,useAsync()演示了它的使用案例。

接下来,我们将解耦钩子并从头开始构建它,解释每一行代码并学习一些巧妙的技巧。

我们还将在第三部分测试一下这个钩子,为什么不呢?

你是不是很期待第二部分的精彩内容?请在评论区留言,并将这篇文章分享给你的朋友们,让他们也兴奋起来!

简单介绍一下我自己,我希望通过创新和高质量的软件让世界变得更美好。
听起来是不是很熟悉?
没错,我是肯特·C·多兹的忠实粉丝,他激励了很多人。

这个钩子在他的 Epic React 工作坊中被广泛使用。快去看看他精彩的课程

未来我也计划通过博客分享我的学习心得,让我们保持联系吧!

作品集 推特 领

也别忘了查看本系列的其他博客文章!

文章来源:https://dev.to/harshkc/i-promise-this-hook-will-blow-your-1000-lines-of-async-code-2cao