使用 React 钩子进行网络请求、记忆和处理错误。

文章出处如下,博主翻译。

https://blog.bitsrc.io/fetching-data-in-react-using-hooks-c6fdd71cb24a

为什么要使用钩子?

类组件冗长而繁琐。在许多情况下,我们被迫在不同的生命周期方法中重复我们的逻辑,以实现我们的 “效果逻辑”。 类组件并没有为组件之间的逻辑共享提供一个优雅的解决方案(HOC 和朋友们也不是一个优雅的解决方案)——另一方面,React Hooks 给我们提供了建立自定义钩子的能力,这是一个更简单的解决方案。

这样的例子不胜枚举。简而言之,我可以说带有钩子的函数组件更 “符合 React 的精神”。它们使分享和重用组件变得更加简单和容易。 作为一个使用云组件中心(如 Bit.dev)为我的团队和开源社区发布和记录组件的人,我可以说,毫无疑问,函数组件更适合分享和重用。

在 Bit.dev 上探索已发布的 React 组件

使用类组件获取数据

在 React 中使用常规类组件时,我们利用生命周期方法从服务器上获取数据并顺利显示。 让我们看一个简单的例子:

class App extends Component {

     this.state = {
          data: []
     }
    componentDidMount() {
        fetch("/api/data").then(
            res => this.setState({...this.state, data: res.data})
        )
    }
    render() {
        return (
            <>
                {this.state.data.map( d => <div>{d}</div>)}
            </>
        )
    }
}

一旦组件被挂载,它将获取数据并渲染。请注意,我们没有把获取逻辑放在构造函数中,而是把它委托给 componentDidMount 钩子。网络请求可能需要一些时间——最好不要阻塞你的组件挂载。 我们解析由 fetch(...) 调用返回的 Promise,并将 data 状态设置为响应数据。反过来,这将重新渲染组件(以显示组件状态中的新数据)。

从一个类组件到一个函数组件

假设我们想将我们的类组件改成一个函数组件。我们将如何实现,使以前的行为保持不变?

useState 和 useEffect

useState 是一个用于维护函数组件中局部状态的钩子。

useEffect 用于在组件渲染后执行函数(以“执行副作用”)。useEffect 可以被限制在选定的一组值发生变化的情况下。这些值被称为 “依赖项”。

useEffects 做的是 componentDidMountcomponentDidUpdatecomponentWillUpdate 组合工作。

这两个钩子本质上给了我们之前从类状态和生命周期方法中得到的所有实用工具。

接下来,让我们将 App 从一个类组件重构为一个函数组件。

function App() {
    const [state, setState] = useState([])
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        )
    })
    return (
        <>
            {state.map( d => <div>{d}</div>)}        
        </>
    )
}

useState 管理一个本地数组状态,state

useEffect 将在组件渲染时发出一个网络请求。当这个请求获得解析时,它将使用 setState 函数把服务器的响应设置为本地状态。这反过来又会导致组件渲染,以便用数据更新 DOM。

使用依赖项防止无休止的回调

我们有一个问题。 useEffect 会在组件挂载和更新时运行。在上述代码中,当 App 挂载时,useEffect 就会运行,然后 setState 就会被调用(在 fetch 请求数据被获取后),但这还不是全部—— useEffect 会因为组件被渲染而被再次触发。可能你也已经发现了,这将在无尽的回调中不断的请求数据。

如前所述,useEffect 有第二个参数,即“依赖项”。这些依赖项指定了在哪些情况下 useEffect 应该对组件的更新做出反应。

依赖项被设置为一个数组。这个数组将包含一些变量,以检查它们在上次渲染后是否有变化。如果其中任何变化,useEffect 就会运行,如果没有变化,useEffect 就不会运行。

useEffect(()=> {
    ...
}, [dep1, dep2])

一个空的依赖数组可以确保在组件被装载时只运行一次 useEffect

function App() {
    const [state, setState] = useState([])
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        )
    }, [])
    return (
        <>
            {state.map( d => <div>{d}</div>)}      
        </>
    )
}

现在,这个函数组件的实现与我们最初的常规类组件实现相同了。两者都将在挂载时运行以获取数据,然后在随后的更新中什么都不做。

使用依赖项进行记忆(memoization)

在计算机领域,记忆(memoization)是主要用于加速程序计算的一种优化技术,它使得函数避免重复演算之前已被处理过的输入,而返回已缓存的结果。 — wikipedia

让我们看一个案例,我们可以使用依赖关系来记忆 useEffect

假设我们有一个从 query 中获取数据的组件。

function App() {
    const [state, setState] = useState([])
    const [query, setQuery] = useState()
    useEffect(() => {
        fetch("/api/data?q=" + query).then(
            res => setState(res.data)
        )
    }, [query])
    function searchQuery(evt) {
        const value = evt.target.value
        setQuery(value)
    }
    return (
        <>
            {state.map( d => <div>{d}</div>)}
            <input type="text" placeholder="Type your query" onEnter={searchQuery} />
        </>
    )
}

我们有一个 query 状态来保存将被发送到 API 的 query 参数。

我们通过将 query 状态传递给依赖数组来记忆 useEffect。这将使 useEffect 在更新/重新渲染时加载 query 的数据,只有当 query 发生变化时才会加载。

如果没有这种记忆,即使 query 没有改变,useEffect也会不断地从端点加载数据,这将导致组件中不必要的重新渲染。

因此,我们有了一个基本的实现,即我们如何使用钩子在函数性 React 组件中获取数据:useState 和 useEffect

useState 用于维护组件中服务器的响应数据。

useEffect 钩子是我们用来从服务器获取数据的(因为它的一个副作用),也给了我们只有普通类组件才有的生命周期钩子,所以我们可以在挂载和更新时获取/更新数据。

错误处理

没有什么是没有错误的。我们在上一节中使用钩子设置了数据获取,太棒了。但是如果获取请求返回时出现了一些错误,会发生什么?App 组件如何响应?

我们需要在组件的数据获取中处理错误。

类组件中的错误处理

让我们看看如何在一个类组件中做到这一点:

class App extends Component {
    constructor() {
        this.state = {
            data: [],
            hasError: false
        }
    }
    componentDidMount() {
        fetch("/api/data").then(
            res => this.setState({...this.state, data: res.data})
        ).catch(err => {
            this.setState({ hasError: true })
        })
    }
    render() {
        return (
            <>
                {this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))}
            </>
        )
    }
}

现在,我们在本地状态中添加了一个 hasError,默认值为 false(是的,它应该是 false,因为,在组件的初始化阶段,还没有发生数据的获取)。 在渲染方法中,我们使用了一个三元操作符来检查组件状态中的 hasError 标志。此外,我们还在 fetch 调用中添加了一个 catch promise,当数据获取失败时,将 hasError 状态设置为 true。

函数组件中的错误处理

让我们来看看一个函数组件对应的实现:

function App() {
    const [state, setState] = useState([])
    const [hasError, setHasError] = useState(false)
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        ).catch(err => setHasError(true))
    }, [])
    return (
        <>
            {hasError? <div>Error occured.</div> : (state.map( d => <div>{d}</div>))}      
        </>
    )
}

添加“ Loading... ”指示器

在类组件中的 Loading

让我们看看在一个类组件中的实现:

class App extends Component {
    constructor() {
        this.state = {
            data: [],
            hasError: false,
            loading: false
        }
    }
    componentDidMount() {
        this.setState({loading: true})
        fetch("/api/data").then(
            res => {
                this.setLoading({ loading: false})
                this.setState({...this.state, data: res.data})
                }
        ).catch(err => {
            this.setState({loading: false})
            this.setState({ hasError: true })
        })
    }
    render() {
        return (
            <>
                {
                    this.state.loading ? <div>loading...</div> : this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))}
            </>
        )
    }
}

我们声明一个状态来保存 loading 标志。然后,在 componentDidMount 中,它将 loading 标志设置为 true,这将导致组件重新渲染以显示“loding…”。

在函数组件的 loading

让我们来看看对应函数组件的实现:

function App() {
    const [state, setState] = useState([])
    const [hasError, setHasError] = useState(false)
    const {loading, setLoading} = useState(false)
    useEffect(() => {
        setLoading(true)
        fetch("/api/data").then(
            res => {
                setState(res.data);
                setLoading(false)}
        ).catch(err => {
            setHasError(true))
            setLoading(false)})
    }, [])
    return (
        <>
            {
                loading ? <div>Loading...</div> : hasError ? <div>Error occured.</div> : (state.map( d => <div>{d}</div>))
            }
        </>
    )
}

这将与之前的类组件的工作方式相同。 我们使用 useState 添加了另一个状态。这个状态将保存 loading 的标识。

它最初被设置为 false,所以当 App 加载时,useEffect 将把它设置为 true(并且会出现 “Loading…”)。然后,在获取数据或发生错误后,加载状态被设置为 false,所以 “Loading… “消失了,取而代之的是 Promise 返回的对应结果。

将所有内容打包在一个 Node 模块中

让我们将我们所做的一切绑定到 Node 模块中。我们将制作一个自定义钩子,用于从函数组件的端点获取数据。

function useFetch(url, opts) {
    const [response, setResponse] = useState(null)
    const [loading, setLoading] = useState(false)
    const [hasError, setHasError] = useState(false)
    useEffect(() => {
        setLoading(true)
        fetch(url, opts)
            .then((res) => {
            setResponse(res.data)
            setLoading(false)
        })
            .catch(() => {
                setHasError(true)
                setLoading(false)
            })
    }, [ url ])
    return [ response, loading, hasError ]
}

我们有了它,useFetch 是一个自定义钩子,用于函数组件的数据获取。我们把我们处理过的每一个主题都合并到一个单一的自定义钩子中。 useFetch 通过向依赖数组传递 url 参数,对将被获取数据的URL 进行记忆。 useEffcect 将总是在传递新的 URL 时运行。 我们可以在我们的函数组件中使用这个自定义钩子。

function App() {
    const [response, loading, hasError] = useFetch("api/data")
    return (
        <>
            {loading ? <div>Loading...</div> : (hasError ? <div>Error occured.</div> : (response.map(data => <div>{data}</div>)))}
        </>
    )
}

简单。

总结

我们已经看到了如何使用 useState 和 useEffect 钩子来从函数组件的 API 端点获取和维护数据。 不要忘了在下面写下你的建议、评论、注释、更正,或者你可以用 DM 或发送电子邮件。

谢谢!!