ホームページ > 記事 > ウェブフロントエンド > React Hooksでデータをリクエストする方法(詳細説明)
この記事の内容はReact Hooksでのデータリクエスト方法(詳細解説)ですので、ある程度の参考価値はありますが、困っている友人は参考にしていただければ幸いです。
このチュートリアルでは、状態フックとエフェクトフックを使用して React でデータをリクエストする方法を説明したいと思います。有名な Hacker News API を使用して、人気のある記事をいくつか取得します。すべてのアプリケーションで再利用したり、npm に公開したりできる独自のデータ リクエスト フックを定義します。
React のこれらの新機能をご存じない場合は、React Hooks についての私の他の記事をご覧ください。記事の例を直接確認したい場合は、この Github リポジトリを直接チェックアウトできます。
注: React の将来のバージョンでは、データの取得にフックは使用されなくなり、Suspense と呼ばれるものに置き換えられます。それにもかかわらず、次の方法は、状態フックと効果フックについて学ぶのに良い方法です。
データ リクエストに React フックを使用する
React でのデータ リクエストの経験がない場合は、私の記事「React でデータをフェッチする方法」を参照してください。この記事では、クラス コンポーネントを使用してデータを取得する方法、再利用可能な Render Props コンポーネントと高次コンポーネントの使用方法、エラーと読み込み状態の処理方法について説明します。この記事では、Function コンポーネントと React Hooks を使用してこれを再作成したいと思います。
import React, { useState } from 'react'; function App() { const [data, setData] = useState({ hits: [] }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
App コンポーネントには、Hacker News 記事からの情報のリストが表示されます。状態および状態更新関数は、useState と呼ばれる状態フックを通じて生成されます。このフックは、リクエストを通じて取得されたアプリ コンポーネントのローカル状態を管理します。初期状態は空の配列であり、現時点では新しい状態を設定する場所がありません。
データの取得には axios を使用しますが、使い慣れたリクエスト ライブラリやブラウザに付属のフェッチ API を使用することももちろん可能です。 axios をまだインストールしていない場合は、npm install axios を通じてインストールできます。
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( 'http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
useEffect エフェクト フックでは、axios を通じて API からデータを取得し、ステート フックの更新関数を使用してデータをローカル ステートに保存します。そして、async/await を使用して Promise を解決します。
しかし、上記のコードを実行すると、無限ループに陥ってしまいます。エフェクトフックは、コンポーネントがマウントおよび更新されるときに実行されます。データを取得するたびに状態が更新されるため、コンポーネントは更新されてエフェクトを再度実行し、データを何度も要求することになります。明らかにそのようなバグを避ける必要があり、コンポーネントがマウントされたときにのみデータをリクエストしたいと考えています。エフェクト フックによって提供される 2 番目のパラメーターに空の配列を渡すことができます。これにより、コンポーネントの更新時にエフェクト フックが実行されることを回避できますが、コンポーネントはマウント時にエフェクト フックを実行します。
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( 'http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
2 番目のパラメータは、フックが依存する変数を定義するために使用されます。変数の 1 つが変更されると、フックが自動的に実行されます。 2 番目のパラメーターが空の配列の場合、フックは変数を監視していないため、コンポーネントの更新時に実行されません。
もう 1 つ注意が必要な点があり、コードでは async/await を使用してサードパーティ API から提供されるデータを取得しています。ドキュメントによると、すべての非同期関数は暗黙の Promise を返します:
"非同期関数宣言は、AsyncFunction オブジェクトを返す非同期関数を定義します。非同期関数は、イベント ループを介して非同期に動作する関数です。暗黙的な Promise を使用して結果を返します。 ""async 関数は、非同期関数オブジェクトを返す非同期関数を定義します。非同期関数は、暗黙的な Promise を使用して、イベント ループを通じて動作する関数です。最終結果を返します。 ."
ただし、エフェクト フックは何も返さないか、クリーンアップ関数を返す必要があります。そのため、コンソールにエラー メッセージが表示されます。
index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.
これは、useEffect 関数で async を直接使用できないことを意味します。効果的なフックで非同期関数を使用できるようにするソリューションを実装してみましょう。
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( 'http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
これは、データ リクエストに React Hooks を使用する小規模なケースです。ただし、エラー処理、読み込み状態、フォーム データの取得をトリガーする方法、および課題処理フックを再利用する方法に興味がある場合は、読み進めてください。
フックを手動または自動でトリガーするにはどうすればよいですか?
コンポーネントのマウント後にデータを取得できるようになりましたが、入力ボックスを使用して API に関心のあるトピックを選択するように動的に指示するにはどうすればよいでしょうか?前のコードからわかるように、デフォルトでは「Redux」をクエリ パラメータとして使用します ('http://hn.algolia.com/api/v1/...')。しかし、React に対してクエリを実行するにはどうすればよいですか?関連トピック? 「Redux」以外のトピックを取得するために、入力ボックスを実装してみましょう。次に、入力ボックスに新しい状態を導入しましょう。
import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( 'http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); } export default App;
现在,请求数据和查询参数两个 state 相互独立,但是我们需要像一个办法希望他们耦合起来,只获取输入框输入的参数指定的话题文章。通过以下修改,组件应该在 mount 之后按照查询获取相应文章。
... function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://hn.algolia.com/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, []); return ( ... ); } export default App;
实际上,我们还缺少部分代码。你会发现当你在输入框输入内容后,并没有获取到新的数据。这是因为 useEffect 的第二个参数只是一个空数组,此时的 effect 不依赖于任何的变量,所以这只会在 mount 只会触发一次。但是,现在我们需要依赖查询条件,一旦查询发送改变,数据请求就应该再次触发。
... function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://hn.algolia.com/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, [query]); return ( ... ); } export default App;
好了,现在一旦你改变输入框内容,数据就会重新获取。但是现在又要另外一个问题:每次输入一个新字符,就会触发 effect 进行一次新的请求。那么我们提供一个按钮来手动触发数据请求呢?
function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [search, setSearch] = useState('redux'); useEffect(() => { const fetchData = async () => { const result = await axios( `http://hn.algolia.com/api/v1/search?query=${search}`, ); setData(result.data); }; fetchData(); }, [search]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setSearch(query)}> Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); }
此外,search state 的初始状态也是设置成了与 query state 相同的状态,因为组件在 mount 的时候会请求一次数据,此时的结果也应该是反应的是输入框中的搜索条件。然而, search state 和 query state 具有类似的值,这看起来比较困惑。为什么不将真实的 URL 设置到 search state 中呢?
function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'http://hn.algolia.com/api/v1/search?query=redux', ); useEffect(() => { const fetchData = async () => { const result = await axios(url); setData(result.data); }; fetchData(); }, [url]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`) } > Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> ); }
这就是通过 effect hook 获取数据的案例,你可以决定 effect 取决于哪个 state。在这个案例中,如果 URL 的 state 发生改变,则再次
运行该 effect 通过 API 重新获取主题文章。
Loading 态 与 React Hooks
让我们在数据的加载过程中引入一个 Loading 状态。它只是另一个由 state hook 管理的状态。Loading state 用于在 App 组件中呈现 Loading 状态。
import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { setIsLoading(true); const result = await axios(url); setData(result.data); setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`) } > Search </button> {isLoading ? ( <p>Loading ...</p> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App;
现在当组件处于 mount 状态或者 URL state 被修改时,调用 effect 获取数据,Loading 状态就会变成 true。一旦请求完成,Loading 状态就会再次被设置为 false。
错误处理与 React Hooks
通过 React Hooks 进行数据请求时,如何进行错误处理呢? 错误只是另一个使用 state hook 初始化的另一种状态。一旦出现错误状态,App 组件就可以反馈给用户。当使用 async/await 函数时,通常使用 try/catch 来进行错误捕获,你可以在 effect 中进行下面操作:
... const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> ... {isError && <p>Something went wrong ...</p>} ... <Fragment> );
effect 每次运行都会重置 error state 的状态,这很有用,因为每次请求失败后,用户可能重新尝试,这样就能够重置错误。为了观察代
码是否生效,你可以填写一个无用的 URL ,然后检查错误信息是否会出现。
使用表单进行数据获取
什么才是获取数据的正确形式呢?现在我们只有输入框和按钮进行组合,一旦引入更多的 input 元素,你可能想要使用表单来进行包装。此外表单还能够触发键盘的 “Enter” 事件。
function App() { ... const doFetch = (evt) => { evt.preventDefault(); setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`); } return ( <Fragment> <form onSubmit={ doFetch } > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <p>Something went wrong ...</p>} ... </Fragment> ); }
自定义 hook 获取数据
我们可以定义一个自定义的 hook,提取出所有与数据请求相关的东西,除了输入框的 query state,除此之外还有 Loading 状态、错误处理。还要确保返回组件中需要用到的变量。
const useHackerNewsApi = () => { const [data, setData] = useState({ hits: [] }); const [url, setUrl] = useState( 'http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = () => { setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`); }; return { data, isLoading, isError, doFetch }; }
现在,我们在 App 组件中使用我们的新 hook 。
function App() { const [query, setQuery] = useState('redux'); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> ... </Fragment> ); }
接下来,在外部传递 URL 给 DoFetch
方法。
const useHackerNewsApi = () => { ... useEffect( ... ); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch }; }; function App() { const [query, setQuery] = useState('redux'); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> <form onSubmit={event => { doFetch( `http://hn.algolia.com/api/v1/search?query=${query}`, ); event.preventDefault(); }} > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> ... </Fragment> ); }
初始的 state 也是通用的,可以通过参数简单的传递到自定义的 hook 中:
import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; const useDataApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch }; }; function App() { const [query, setQuery] = useState('redux'); const { data, isLoading, isError, doFetch } = useDataApi( 'http://hn.algolia.com/api/v1/search?query=redux', { hits: [] }, ); return ( <Fragment> <form onSubmit={event => { doFetch( `http://hn.algolia.com/api/v1/search?query=${query}`, ); event.preventDefault(); }} > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <p>Something went wrong ...</p>} {isLoading ? ( <p>Loading ...</p> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App;
这就是使用自定义 hook 获取数据的方法,hook 本身对API一无所知,它从外部获取参数,只管理必要的 state ,如数据、 Loading 和
错误相关的 state ,并且执行请求并将数据通过 hook 返回给组件。
用于数据获取的 Reducer Hook
目前为止,我们已经使用 state hooks 来管理了我们获取到的数据数据、Loading 状态、错误状态。然而,所有的状态都有属于自己的 state hook,但是他们又都连接在一起,关心的是同样的事情。如你所见,所有的它们都在数据获取函数中被使用。它们一个接一个的被调用(比如:setIsError、setIsLoading),这才是将它们连接在一起的正确用法。让我们用一个 Reducer Hook 将这三者连接在一起。
Reducer Hook 返回一个 state 对象和一个函数(用来改变 state 对象)。这个函数被称为分发函数(dispatch function),它分发一个 action,action 具有 type 和 payload 两个属性。所有的这些信息都在 reducer 函数中被接收,根据之前的状态提取一个新的状态。让我们看看在代码中是如何工作的:
import React, { Fragment, useState, useEffect, useReducer, } from 'react'; import axios from 'axios'; const dataFetchReducer = (state, action) => { ... }; const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); ... };
Reducer Hook 以 reducer 函数和一个初始状态对象作为参数。在我们的案例中,加载的数据、Loading 状态、错误状态都是作为初始状态参数,且不会发生改变,但是他们被聚合到一个状态对象中,由 reducer hook 管理,而不是单个 state hooks。
const dataFetchReducer = (state, action) => { ... }; const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { const result = await axios(url); dispatch({ type: 'FETCH_SUCCESS', payload: result.data }); } catch (error) { dispatch({ type: 'FETCH_FAILURE' }); } }; fetchData(); }, [url]); ... };
现在,在获取数据时,可以使用 dispatch 函数向 reducer 函数发送信息。使用 dispatch 函数发送的对象具有一个必填的 type 属性和一个可选的 payload 属性。type 属性告诉 reducer 函数需要转换的 state 是哪个,还可以从 payload 中提取新的 state。在这里只有三个状态转换:初始化数据过程,通知数据请求成功的结果,以及通知数据请求失败的结果。
在自定义 hook 的末尾,state 像以前一样返回,但是因为我们所有的 state 都在一个对象中,而不再是独立的 state ,所以 state 对象进行解构返回。这样,调用 useDataApi 自定义 hook 的人仍然可以 data 、isLoading和isError:
const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); ... const doFetch = url => { setUrl(url); }; return { ...state, doFetch }; };
最后我们还缺少 reducer 函数的实现。它需要处理三个不同的状态转换,分被称为 FEATCH_INIT
、FEATCH_SUCCESS
、FEATCH_FAILURE
。每个状态转换都需要返回一个新的状态。让我们看看使用 switch case 如何实现这个逻辑:
const dataFetchReducer = (state, action) => { switch (action.type) { case 'FETCH_INIT': return { ...state }; case 'FETCH_SUCCESS': return { ...state }; case 'FETCH_FAILURE': return { ...state }; default: throw new Error(); } };
reducer 函数可以通过其参数访问当前状态和 dispatch 传入的 action。到目前为止,在 switch case 语句中,每个状态转换只返回前一个状态,析构语句用于保持 state 对象不可变(即状态永远不会被直接更改)。现在让我们重写一些当前 state 返回的属性,以便在每次转换时更改 一些 state:
const dataFetchReducer = (state, action) => { switch (action.type) { case 'FETCH_INIT': return { ...state, isLoading: true, isError: false }; case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, data: action.payload, }; case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: throw new Error(); } };
现在,每个状态转换(action.type决定)都返回一个基于先前 state 和可选 payload 的新状态。例如,在请求成功的情况下,payload 用于设置新 state 对象的 data 属性。
总之,reducer hook 确保使用自己的逻辑封装状态管理的这一部分。通过提供 action type 和可选 payload ,总是会得到可预测的状态更改。此外,永远不会遇到无效状态。例如,以前可能会意外地将 isLoading 和 isError 设置为true。在这种情况下,UI中应该显示什么? 现在,由 reducer 函数定义的每个 state 转换都指向一个有效的 state 对象。
在 Effect Hook 中中断数据请求
在React中,即使组件已经卸载,组件 state 仍然会被被赋值,这是一个常见的问题。我在之前的文章中写过这个问题,它描述了如何防止在各种场景中为未挂载组件设置状态。让我们看看在自定义 hook 中,请求数据时如何防止设置状态:
const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { const result = await axios(url); if (!didCancel) { dispatch({ type: 'FETCH_SUCCESS', payload: result.data }); } } catch (error) { if (!didCancel) { dispatch({ type: 'FETCH_FAILURE' }); } } }; fetchData(); return () => { didCancel = true; }; }, [url]); const doFetch = url => { setUrl(url); }; return { ...state, doFetch }; };
每个Effect Hook都带有一个clean up函数,它在组件卸载时运行。clean up 函数是 hook 返回的一个函数。在该案例中,我们使用 didCancel 变量来让 fetchData 知道组件的状态(挂载/卸载)。如果组件确实被卸载了,则应该将标志设置为 true,从而防止在最终异步解析数据获取之后设置组件状态。
注意:实际上并没有中止数据获取(不过可以通过Axios取消来实现),但是不再为卸载的组件执行状态转换。由于 Axios 取消在我看来并不是最好的API,所以这个防止设置状态的布尔标志也可以完成这项工作。
本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的JavaScript视频教程栏目!
以上がReact Hooksでデータをリクエストする方法(詳細説明)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。