如何使用React Hooks獲取數據?

復讀前言

原文連接前端

這是我在《the Road to React》的做者Robin Wieruchu的博客中看到的一篇關於如何使用React Hooks獲取數據的一篇文章,之因此想翻譯,是以爲這篇文章能夠按部就班地爲讀者介紹,在不一樣的場景下如何使用正確的React Hooks,若是您對React Hooks不是很瞭解,相信讀完這篇文章會對其有新的認識,好吧,閒話很少說,接下來,翻譯正文開始。react


在本教程中,我將向您展現如何使用React Hooks中的state以及effect來獲取數據。咱們將使用廣爲人知的Hacker News API從科技界獲取熱門文章。你還將實現用於數據獲取的自定義鉤子函數,該鉤子函數可在程序中的任何位置複用,或者做爲獨立的包發佈在npm上。ios

若是您對這個新的React功能一無所知,請查看此React Hooks簡介。若是您想查看項目代碼,想知道如何使用React Hooks獲取數據,能夠查看這個代碼倉庫git

若是您只想使用React Hooks進行數據獲取操做,能夠執行npm install use-data-api,請按照文檔進行操做。若是使用它,別忘了給個Star哦。github

使用React Hooks進行數據獲取

若是您不熟悉React中的數據獲取,請查看個人 extensive data fetching in React article。它帶您逐步瞭解如何使用React類組件獲取數據,如何經過Render Prop 組件高階組件使其變爲可複用組件,以及如何進行error處理以及loading狀態處理。在本文中,我想經過函數組件中的React Hooks向您展現這些內容。npm

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組件顯示項目列表(hits = Hacker News文章)。 經過useState,能夠查看state或者更新state,從而管理App組件所獲取的數據,初始狀態中{ hits: [] }表明一個空列表,此時還未進行數據獲取。編程

咱們將使用axios來獲取數據,固然使用其餘第三方庫也是能夠的。若是還沒有安裝axios,則能夠在命令行中使用來安裝npm install axios。而後實現數據獲取的effect hookredux

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'https://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提取數據,並使用useState中的update函數來更新狀態,從而更新視圖,promise經過async/await來處理。axios

可是,當您運行應用程序時,應該陷入一個討厭的循環。useEffect在組件掛載階段運行,同時也在組件update階段運行。由於咱們在每次獲取數據後都要更新state,此時組件會update,那useEffect又會執行,從而又進行state更新(ps:首次加載-> useEffect -> 更新state -> update階段再次執行useEffect -> 再次更新state......無限循環)。它一次又一次地獲取數據,要避免這種錯誤。咱們只想在組件掛載時獲取數據。所以,您能夠爲效果掛鉤提供一個空數組[]做爲第二個參數,以免在組件更新時調用,而僅在組件掛載階段調用它。api

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(async () => {
    const result = await axios(
      'https://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第二個參數可用於定義掛鉤所依賴的全部變量(在此數組中分配)。若是變量之一更改,則掛鉤再次運行。若是帶有變量的數組爲空,則在更新組件時此鉤子函數根本不會運行,由於它沒必要監視任何變量。

最後一招。在代碼中,咱們使用async / await從第三方API獲取數據。根據文檔,每一個帶有async註釋的函數都會返回一個隱含的promise,可是,useEffect應不返回任何內容返回一個清除函數。這就是爲何您可能在開發人員控制檯日誌中看到如下警告:useEffect函數必須返回清除函數,不然不返回任何內容。不支持Promises和useEffect(async()=> ...),可是您能夠在useEffect內部調用異步函數。這就是爲何useEffect不容許直接在函數中使用異步的緣由。讓咱們經過使用useEffect內的異步函數來實現此變通方法。

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(
        'https://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獲取數據。可是若是您對error處理,loading指示器,如何觸發從表單中獲取數據以及如何實現可複用的數據獲取鉤子函數感興趣,請繼續閱讀。

如何手動觸發hooks?

太好了,一旦組件掛載完畢咱們就獲取數據。可是,如何使用輸入字段來告訴API咱們感興趣的主題呢?「 Redux」被用做默認查詢。可是關於「React」的話題呢?讓咱們實現一個input元素,使咱們可以獲取「 Redux」文章之外的其餘文章。所以,爲輸入元素引入新的state。

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(
        'https://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;
複製代碼

目前,這兩種狀態彼此獨立,可是如今您但願它們僅在輸入時獲取指定文章從而關聯起來,進行如下更改後,該組件會在掛載後按輸入的內容查詢全部文章。

...
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}`, // 添加query
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    ...
  );
}
export default App;
複製代碼

還缺乏一件事:當您嘗試在輸入框中輸入內容時,從useEffect觸發以後,就不會再獲取其餘數據。那是由於您提供了空數組[]做爲效果的第二個參數。該反作用不依賴任何變量,所以僅在掛載組件時觸發。可是,如今效果應取決於query的內容。query內容更改後,數據請求應再次觸發。

...
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]); // 這樣useEffect就會在query發生改變時調用
  return (
    ...
  );
}
export default App;
複製代碼

更改輸入內容後,數據應該會從新獲取。但這帶來了另外一個問題:在輸入框鍵入每一個字符時,都會觸發useEffect並執行另外一個數據獲取請求。如何提供一個觸發請求的按鈕,從而手動觸發鉤子?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );
      setData(result.data);
    };
    fetchData();
  }, [query]);
  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>
  );
複製代碼

如今,使useEffect取決於search狀態,而不是隨輸入內容而變化的波動query狀態。用戶單擊按鈕後,便會設置新的search狀態,並應手動觸發反作用鉤子函數。

...
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]); // 此時反作用鉤子函數依賴search狀態,點擊時會觸發反作用鉤子函數。
  return (
    ...
  );
}
export default App;
複製代碼

search的初始狀態也被設置爲與query狀態相同的數據,該組件在掛載階段獲取數據,所以query反映輸入字段中的值。可是,具備相似的query和search狀態有點使人困惑。爲何不將實際URL設置爲狀態而是search狀態?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://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>
  );
}
複製代碼

若是是使用useEffect進行隱式編程數據獲取的話,您能夠決定反作用取決於哪一個狀態,在單擊或其餘反作用上設置此狀態後,該effect hook將再次運行。在這種狀況下,若是URL狀態發生更改,則effect hook將再次運行請求API以獲取數據。

使用React Hooks實現loading指示器

讓咱們爲數據獲取引入一個loading指示器。這只是由state hook管理的另外一個狀態。loading標識用於在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(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false); // 初始設置loading狀態爲false
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true); // 初始設置loading狀態爲true
      const result = await axios(url);
      setData(result.data);
      setIsLoading(false); // 數據返回後設置loading狀態爲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>
      // 經過判斷loading狀態,來決定是否展現loading樣式
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}
export default App;
複製代碼

一旦調用了反作用鉤子函數以進行數據獲取(在組件掛載或URL狀態更改時發生),則loading狀態將設置爲true。請求返回結果後,loading狀態將再次設置爲false。

使用React Hooks進行錯誤處理

使用React Hooks獲取數據的錯誤處理怎麼樣?該錯誤只是使用state hook初始化的另外一個狀態。一旦出現錯誤狀態,App組件便可爲用戶提供反饋。使用async/await時,一般使用try / catch塊進行錯誤處理。您能夠在反作用鉤子函數範圍內作到這一點:

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(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);    // 請求開始設置error狀態爲false
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData(result.data);
      } catch (error) {
        setIsError(true); // 若是catch到錯誤,即設置error狀態爲true
      }
      setIsLoading(false); // 最終loading狀態設置爲false,防止阻塞UI
    };
    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>
      {isError && <div>Something went wrong ...</div>} // error狀態下展現相應UI
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}
export default App;
複製代碼

每次鉤子函數再次運行時,都會重置error狀態。這頗有用,由於在失敗的請求以後,用戶可能想要再次嘗試,因此應該重置error狀態。爲了本身執行錯誤,您能夠將URL更改成無效的內容。而後檢查是否顯示錯誤消息。

使用Form表單和React Hooks獲取數據

如何用適當的形式來獲取數據呢?到目前爲止,咱們只有輸入框和按鈕的組合。引入更多input框後,您可能須要用form表單包裝它們。此外,還能夠經過form使用鍵盤上的「Enter」來觸發按鈕。

function App() {
  ...
  return (
    <Fragment>
      <form
        onSubmit={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
      {isError && <div>Something went wrong ...</div>}
      ...
    </Fragment>
  );
}
複製代碼

可是如今,單擊「提交」按鈕時瀏覽器會從新加載,由於這是瀏覽器提交表單時的默認行爲。爲了防止默認行爲,咱們能夠在React事件上調用一個函數。這也是您在React類組件中執行此操做的方式。

function App() {
  ...
  return (
    <Fragment>
      <form onSubmit={event => {
        setUrl(`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 && <div>Something went wrong ...</div>}
      ...
    </Fragment>
  );
}
複製代碼

如今,單擊「提交」按鈕後,瀏覽器再也不須要刷新。它像之前同樣工做,可是此次使用的是表單而不是樸素的輸入框和按鈕組合。您也能夠按鍵盤上的「 Enter」鍵。

自定義hook獲取數據

爲了抽出獲取數據的自定義鉤子函數,請將屬於數據獲取的全部內容(屬於輸入字段的search狀態(還包括loading指示器和error處理))移至其自身的功能。另外,請確保您從App組件中使用的函數返回全部必需的數據。

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'https://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]);
  return [{ data, isLoading, isError }, setUrl]; // 最終返回全部必需的數據
}
複製代碼

如今,新的鉤子函數能夠再次在App組件中使用:

function App() {
  const [query, setQuery] = useState('redux');
  // 封裝了函數,經過數組解構直接返回數據獲取函數、最終的數據、loading、error這些狀態
  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>
  );
}
複製代碼

初始狀態也能夠設爲通用。將其簡單地傳遞到新的自定義鉤子函數中:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
// 經過參數傳入初始化url和初始化數據,使得數據獲取的自定義鉤子函數更純
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]);
  return [{ data, isLoading, isError }, setUrl];
};
function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    'https://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 && <div>Something went wrong ...</div>}
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}
export default App;
複製代碼

使用自定義鉤子函數獲取數據即將完成。其實鉤子函數自己對API一無所知。它從外部接收全部參數,而且僅管理必要的狀態,例如數據,loading和error狀態。它執行請求並經過數據獲取的自定義鉤子函數,將數據返回給組件。

經過Reducer Hook進行數據獲取

到目前爲止,咱們已經使用各類狀態鉤子函數來管理數據的獲取狀態,loading和error狀態。可是,全部的這些狀態,管理本身的state hook,它們屬於同一類,由於它們關心同一件事情,如您所見,它們都在數據獲取函數中使用。一個好的指示器能夠將一個又一個的state集結起來(例如:數據、loading、error狀態),讓咱們將它們所有三個與一個Reducer Hook結合使用。

Reducer Hook向咱們返回一個state對象和一個更改state對象的函數。該函數--稱爲disapatch,它採用action(擁有一個type和可選的payload)。全部這些信息都在實際的reducer功能中使用,以從先前state,經過操做的type和可選payload和中提取新state。讓咱們看看這在代碼中是如何工做的:

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);
  // useReducer傳入一個type和一個payload對象,經過數組結構得到state和dispatch
  const [state, dispatch] = useReducer(dataFetchReducer, { 
    isLoading: false,
    isError: false,
    data: initialData,
  });
  ...
};
複製代碼

Reducer hook將一個reducer函數以及一個初始化的state對象做爲參數。在咱們的例子中,數據,loading狀態和error狀態的初始狀態的參數並無改變,可是它們已經聚合到一個由reducer鉤子函數(useReducer)而不是單個useState管理的狀態對象中。

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函數發送的action對象具備必填type屬性和可選payload屬性。該類型告訴reducer函數須要應用哪一個狀態轉換,而且reducer能夠額外使用payload屬性來提取新state對象。畢竟,咱們只有三個狀態轉換:初始化獲取過程,通知成功的數據獲取結果,以及通知錯誤的數據獲取結果。

在自定義鉤子函數的末尾,state將像之前同樣返回,可是由於咱們有一個state對象,而再也不是獨立state。這樣一來,誰調用了一個useDataApi自定義的鉤子函數仍然獲得訪問data,isLoading以及isError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  ...
  return [state, setUrl]; // 最終返回的state包含data、isError、isLoading
};
複製代碼

最後一點,也是至關重要的一點,咱們還缺乏reducer函數的實現。它須要根據action對象中三種不一樣的type(FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE),來進行三種不一樣的狀態轉換,每一次轉換都須要返回一個新的state對象,讓咱們看看如何用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函數能夠經過其參數訪問state和action。到目前爲止,在switch case語句中,每一個狀態轉換僅返回先前的狀態。對象解構可讓state對象保持不變-意味着state永遠不會直接發生變化-來執行最佳實踐。如今,讓咱們重寫一些當前state返回的屬性,以在每次狀態轉換時更改state:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state, // 解構讓咱們能夠保持原先state不變
        isLoading: true, // 只有當咱們手動重寫state中的特定屬性時,纔會發生改變,這樣很安全
        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對象的數據。

總之,Reducer Hook確保狀態管理的這一部分使用其本身的邏輯進行封裝。經過提供action對象中的type和可選的payload,您將始終返回一個新的數據實現狀態變動。此外,您將永遠不會陷入無效狀態。例如,之前可能會意外地將isLoadingand isError狀態同時設置爲true。在這種狀況下,UI中應顯示什麼?如今,由reducer函數定義的每一個狀態轉換聲明都衍生出一個有效的state對象。

停止Effect Hook的數據獲取

在React中,一個常見的問題是即便已卸載組件,會被設置state(例如,因爲使用React Router導航)。在此以前,我已經寫過有關此問題的文章,它描述了如何防止已卸載組件設置state。讓咱們看看如何防止在自定義鉤子函數中,在已卸載階段仍然設置state:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  useEffect(() => {
    let didCancel = false; // useEffect執行時設置初始化didCancel爲false
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        if (!didCancel) { // 只有在didCancel爲false的狀況下,纔會進行狀態變動
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) { // 同上
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
    fetchData();
    return () => {
    // 重點:在useEffect中,若是最終返回一個清除函數,即該函數會在組件卸載階段執行, 防止組件卸載後仍然設置狀態
      didCancel = true; 
    };
  }, [url]);
  return [state, setUrl];
};
複製代碼

每一個Effect Hook都帶有清理功能,該功能在卸載組件時運行。清理函數是從鉤子函數中返回的一個函數。在咱們的例子中,咱們使用一個boolean標識didCancel來使咱們的數據獲取邏輯知道組件的state(掛載/卸載)。若是確實卸載了組件,則應該設置該標識true,以防止在最終異步地解析了數據獲取以後設置組件state。

注意:實際上,數據獲取不會停止-這能夠經過Axios Cancellation來實現-但已卸載的組件再也不執行狀態轉換。因爲Axios Cancellation在我眼中並非最好的API,所以該防止設置狀態的boolean標識也能夠完成這項工做。

您已經瞭解了state和effect的React鉤子函數如何在React中用於數據獲取。若是您對使用render props和higher-order components(高階組件)在類組件(和函數組件)中的數據獲取感到好奇,請從頭開始閱讀個人其餘文章。在此,我但願本文對您瞭解React Hooks以及如何在實際場景中使用它們頗有幫助。


復讀結語

到這裏也算翻譯完成了,但願閱讀本文後,你能夠對React Hooks有一個新的認識,若是你有以爲很是優秀的英文前端文章,但願能夠翻譯成中文的話,能夠在留言評論,我會幫忙翻譯一些你們都比較感興趣的文章。

相關文章
相關標籤/搜索