Der Inhalt dieses Artikels befasst sich mit der Anforderung von Daten in React Hooks (ausführliche Erklärung). Freunde in Not können darauf verweisen.
In diesem Tutorial möchte ich Ihnen erklären, wie Sie Status- und Effekt-Hooks verwenden, um Daten in React anzufordern. Wir werden die bekannte Hacker News API verwenden, um einige beliebte Artikel zu erhalten. Sie definieren Ihre eigenen Datenanforderungs-Hooks, die in allen Ihren Anwendungen wiederverwendet oder in npm veröffentlicht werden können.
Wenn Sie diese neuen Funktionen von React nicht kennen, können Sie sich meinen anderen Artikel zur Einführung zu React Hooks ansehen. Wenn Sie das Beispiel des Artikels direkt sehen möchten, können Sie dieses Github-Repository direkt auschecken.
Hinweis: In zukünftigen Versionen von React werden Hooks nicht mehr zum Abrufen von Daten verwendet, sondern durch etwas namens Suspense ersetzt. Nichtsdestotrotz ist die folgende Methode immer noch eine gute Möglichkeit, etwas über Zustands- und Wirkungs-Hooks zu lernen.
Verwenden Sie React Hooks für Datenanfragen
Wenn Sie keine Erfahrung mit Datenanfragen in React haben, können Sie meinen Artikel lesen: So rufen Sie Daten in React ab. Der Artikel erklärt, wie man Klassenkomponenten zum Abrufen von Daten verwendet, wie man wiederverwendbare Render-Props-Komponenten und Komponenten höherer Ordnung verwendet und wie man mit Fehlern und Ladezuständen umgeht. In diesem Artikel möchte ich dies mithilfe von Funktionskomponenten und React Hooks nachbilden.
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;
Die App-Komponente zeigt eine Liste mit Informationen aus Hacker-News-Artikeln an. Status- und Statusaktualisierungsfunktionen werden über einen Status-Hook namens useState generiert, der für die Verwaltung des lokalen Status der durch Anforderungen erhaltenen App-Komponente verantwortlich ist. Der Anfangszustand ist ein leeres Array und es gibt derzeit keinen Ort, an dem ein neuer Zustand dafür festgelegt werden kann.
Wir werden Axios verwenden, um Daten abzurufen. Natürlich können Sie auch die Ihnen bekannte Anforderungsbibliothek oder die mit dem Browser gelieferte Abruf-API verwenden. Wenn Sie Axios noch nicht installiert haben, können Sie es über npm install axios installieren.
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;
Im useEffect-Effekt-Hook erhalten wir Daten von der API über Axios und verwenden die Aktualisierungsfunktion des State-Hooks, um die Daten im lokalen Status zu speichern. Und verwenden Sie async/await, um Versprechen aufzulösen.
Wenn Sie jedoch den obigen Code ausführen, bleiben Sie in einer verdammten Endlosschleife stecken. Effekt-Hooks werden ausgeführt, wenn Komponenten gemountet und aktualisiert werden. Denn jedes Mal, wenn wir Daten erhalten, aktualisieren wir den Status, sodass die Komponente aktualisiert und den Effekt erneut ausführt, wodurch die Daten immer wieder angefordert werden. Offensichtlich müssen wir solche Fehler vermeiden. Wir möchten Daten nur anfordern, wenn die Komponente gemountet ist. Sie können im zweiten vom Effekt-Hook bereitgestellten Parameter ein leeres Array übergeben. Dadurch kann vermieden werden, dass der Effekt-Hook ausgeführt wird, wenn die Komponente aktualisiert wird. Die Komponente führt ihn jedoch weiterhin aus, wenn sie gemountet wird.
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;
Der zweite Parameter wird verwendet, um die Variablen zu definieren, von denen der Hook abhängt. Wenn sich eine der Variablen ändert, wird der Hook automatisch ausgeführt. Wenn der zweite Parameter ein leeres Array ist, wird der Hook bei Komponentenaktualisierungen nicht ausgeführt, da er keine Variablen überwacht.
Es gibt einen weiteren Punkt, der besondere Aufmerksamkeit erfordert. Im Code verwenden wir async/await, um die von der Drittanbieter-API bereitgestellten Daten abzurufen. Laut Dokumentation gibt jede asynchrone Funktion ein implizites Versprechen zurück:
„Die Deklaration der asynchronen Funktion definiert eine asynchrone Funktion, die ein AsyncFunction-Objekt zurückgibt. Eine asynchrone Funktion ist eine Funktion, die asynchron über die Ereignisschleife arbeitet. „“Die asynchrone Funktion definiert eine asynchrone Funktion, die ein asynchrones Funktionsobjekt zurückgibt. Die asynchrone Funktion ist eine Funktion, die durch die Ereignisschleife arbeitet und ein implizites Versprechen zurückgibt . ”
Der Effekt-Hook sollte jedoch nichts oder eine Aufräumfunktion zurückgeben. Aus diesem Grund wird in der Konsole eine Fehlermeldung angezeigt.
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.
Das bedeutet, dass wir async nicht direkt in der useEffect-Funktion verwenden können. Lassen Sie uns eine Lösung implementieren, um asynchrone Funktionen in Effekt-Hooks verwenden zu können.
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;
Dies ist ein kleiner Fall, bei dem React Hooks für Datenanfragen verwendet werden. Wenn Sie sich jedoch für die Fehlerbehandlung, den Ladezustand, das Auslösen der Formulardatenerfassung und die Wiederverwendung des Issue-Verarbeitungs-Hooks interessieren, dann lesen Sie weiter.
Wie löst man einen Hook manuell oder automatisch aus?
Jetzt können wir die Daten abrufen, nachdem die Komponente gemountet wurde, aber wie kann man das Eingabefeld verwenden, um die API dynamisch anzuweisen, ein Thema von Interesse auszuwählen? Wie Sie dem vorherigen Code entnehmen können, verwenden wir standardmäßig „Redux“ als Abfrageparameter ('http://hn.algolia.com/api/v1/...'), aber wie fragen wir nach React- verwandte Themen? Lassen Sie uns ein Eingabefeld implementieren, um neben „Redux“ auch andere Themen zu erhalten. Fügen wir nun dem Eingabefeld einen neuen Status hinzu.
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视频教程栏目!
Das obige ist der detaillierte Inhalt vonSo fordern Sie Daten in React Hooks an (ausführliche Erklärung). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!