[譯]如何用React Hooks獲取數據

如何用React Hooks獲取數據

前言

  • 注意:筆者將會使用TypeScript來完成文章中的代碼,並使用ant design來美化界面樣式
  • 若是文章對你有用的話,歡迎`star`

在這個教程中,我想向您展現如何經過stateeffect鉤子來獲取數據。咱們將會使用廣爲人知的Hacker News API從科技世界獲取流行文章。經過這篇文章,你也能爲數據獲取實現本身的自定義hook,它能夠在你的應用中的任何地方被複用或者發佈到npm做爲一個獨立的node包。javascript

若是你徹底不瞭解這個React新特性,查閱這裏:introduction to React Hooks。若是你想要查閱如何在React中使用hooks來獲取數據示例的完成項目,能夠查閱這個GitHub倉庫html

若是你只想準備去使用React Hook來獲取數據:執行npm install use-data-api而且跟隨文檔來使用。若是你用到了它,別忘記爲它點一個starjava

注意:在將來,React並不打算用React Hooks來獲取數據。相反的,一個叫作Suspense的特性將會負責這件事。不過,下列的講解是一個很好的用來學習更多關於Reactstateeffect hooks內容的方式。node

用`React Hooks`來獲取數據

若是你不熟悉React中的數據獲取,能夠查閱個人文章React中如何數據獲取。它將帶你瞭解如何使用React類組件來進行數據獲取,如何使用Render Prop Components高階組件(Higher-Order Component s)來使代碼變的可複用,如何進行錯誤處理和加載loading狀態。在這篇文章,我想用函數組件中的React Hooks來向你展現這些內容。react

import React, { useEffect, useState } from 'react';

const App: React.FC = () => {
  const [data, setData] = useState<{ hits: any[] }>({ 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 articles)。state和更新state的函數來自於一個叫作useStatehook,它負責管理咱們要爲App組件取回數據的局部狀態。初始狀態用一個對象中空的hits數組來表示列表數據,尚未人爲該數據設置任何狀態。ios

咱們將要使用axios來獲取數據,可是想要使用其它的數據獲取庫或者瀏覽器原生的fetch API將取決於你。若是你尚未安裝axios, 你能夠經過命令行使用npm install axios來作這件事,而後實現獲取數據的effect hookgit

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const App: React.FC = () => {
  const [data, setData] = useState<{ hits: any[] }>({ hits: [] });
  useEffect(async() => {
    const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
    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;
複製代碼

effect hook叫作useEffect,它能夠用axiosAPI獲取數據而且使用組件state hook的更新函數來設置該組件局部狀態的數據。這裏咱們用async/await來解決異步的Promisegithub

然而,當你運行你的應用的時候,你應該會陷入一個討厭的循環。effect hook不只會在組件掛載後運行,也會在組件更新後運行。因爲咱們在獲取數據後使用setData設置了state,這會更新組件而且使effect再次運行。而當effect再次運行的時候,它又會再次獲取數據並調用setData,周而復始。這是一個須要避免的bug咱們只想在組件渲染後獲取數據。這就是爲何要爲effect hook傳一個空數組([])做爲第二個參數的緣由:避免effect hook在組件更新後激活而僅僅是在組件掛載後激活。web

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const App: React.FC = () => {
  const [data, setData] = useState<{ hits: any[] }>({ hits: [] });
  useEffect(async() => {
    const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
    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;
複製代碼

第二個參數用來定義hook依賴的全部變量(分配給這個數組的)。若是其中的一個變量發生改變,hook將會再次運行。若是數組中的的變量爲空,hook在組件更新後將再也不運行,由於它沒有必要監測任何變量。npm

還有最後一個陷阱。在代碼中,咱們使用async/await從第三方API獲取數據。按照文檔所說,每一個用async標註的函數都會返回一個隱式的promise: "async函數聲明定義一個返回AsyncFunction對象的異步函數。異步函數是經過事件循環異步執行而且使用一個隱式的promise返回結果的函數"。然而,一個effect hook應該什麼都不返回或者返回一個清理函數。這就是爲何你可能會在你的開發者控制檯日誌看到以下警告:07:41:22.910 index.js 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函數必須返回一個清理函數或者什麼都不返回。PromisesuseEffect(async() => ...))是不支持的,可是你能夠在effect內部調用一個async函數).這也是爲何在useEffect函數裏直接使用async關鍵字不被容許的緣由。讓咱們爲這種狀況想一種變通方案:在effect內部使用async函數。(譯註者:下邊的代碼使用了ant design,以後再也不提示)

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
      setData({ hits: result.data.hits });
    };
    fetchData().then();
  }, []);
  return (
    <Card bordered={false}>
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

譯註者:hacker news的響應數據TypeScript類型以下:

// responseTypes.ts
export interface IData {hits: IHit[];}

interface IHit {
  title: string;
  url: string;
  author: string;
  points: number;
  story_text?: any;
  comment_text?: any;
  _tags: string[];
  num_comments: number;
  objectID: string;
  _highlightResult: IHighlightResult;
}

interface IHighlightResult {
  title: ITitle;
  url: ITitle;
  author: IAuthor;
}

interface IAuthor {
  value: string;
  matchLevel: string;
  matchedWords: string[];
}

interface ITitle {
  value: string;
  matchLevel: string;
  matchedWords: any[];
}
複製代碼

簡單來講,這就是使用React hooks來獲取數據。可是若是你對錯誤處理、展現loading狀態、如何觸發從表單獲取數據、以及如何實現一個可複用的數據獲取hook感興趣的話,請繼續閱讀。

如何手動地/編程式地觸發一個`Hook`

很好,一旦組件掛載咱們就會去獲取數據。可是使用一個輸入字段來告訴API咱們對哪一個話題感興趣呢?"Redux"被做爲默認查詢,可是關於"React"的話題呢?接下來,讓咱們實現一個input輸入框,可以讓用戶獲取除"Redux"外的其它相關文章。所以,咱們爲輸入元素引入一個新的狀態。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  const [query, setQuery] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=redux');
      setData({ hits: result.data.hits });
    };
    fetchData().then();
  }, []);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

此時,倆個狀態彼此之間互相獨立,可是如今你想要組合它們只獲取輸入框中指定查詢條件的文章數據。一旦組件掛載後,應該經過查詢項來獲取全部文章。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  const [query, setQuery] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
      setData({ hits: result.data.hits });
    };
    fetchData().then();
  }, []);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

這裏有一起被忽略了:當你嘗試在輸入框中輸入一些文字後,組件從新渲染後並無進行數據獲取。那是由於你提供了空數組做爲effect的第二個參數,effect不依賴於任何變量,所以它只會在組件掛載後觸發。然而,effect如今應該依賴於query,一旦query改變,數據請求將會再次觸發。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  const [query, setQuery] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
      setData({ hits: result.data.hits });
    };
    fetchData().then();
  }, [query]);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

一旦你改變了輸入框中的值數據將會從新獲取。可是這也引起了另一個問題:在你每爲輸入框輸入一個字符的時候,effect將會被觸發而且執行另外一次數據獲取請求。那麼如何提供一個按鈕來觸發請求從而手動的地執行hook呢?

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(`https://hn.algolia.com/api/v1/search?query=${search}`);
      setData({ hits: result.data.hits });
    };
    fetchData().then();
  }, [search]);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Button onClick={() => setSearch(query)}>Search</Button>
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

如今,咱們使effect依賴於search狀態而不是隨着在輸入框中每一次擊鍵而發生變化的query狀態。一旦用戶點擊按鈕,新的search狀態被設置而且手動地觸發effect hook

search狀態的初始值也被設置爲和query狀態同樣。由於組件也在掛載後獲取數據,而且結果應該和輸入框中的值做爲查詢條件獲取到的數據相同。然而,具備相似的querysearch狀態有一點讓人疑惑。爲何不設置當前的請求地址做爲狀態來替代search狀態?

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ 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({ hits: result.data.hits });
    };
    fetchData().then();
  }, [url]);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
      <List dataSource={data.hits} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

上面的例子就是用effect hook編程地/手動地獲取數據。你能夠決定effect依賴於哪個狀態。一旦你在點擊事件或在其它的反作用(useEffect)中設置狀態(setState),對應的effect將會再次運行。在上邊的例子中,若是url狀態發生改變,effect將會再次運行並從API中獲取文章數據。

用`React Hooks`來顯示`loading`

讓咱們繼續介紹數據獲取時的loading展現。本質上,loading只是被state hook所管理的另一個state。在App組件中,loading標識用來渲染一個loading指示器。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState('https://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({ hits: result.data.hits });
      setIsLoading(false);
    };
    fetchData().then();
  }, [url]);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
      <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
        <List.Item>
          <a href={item.url}>{item.title}</a>
        </List.Item>
      )}/>
    </Card>
  );
};
export default App;
複製代碼

當組件掛載或者url狀態發生改變,effect被調用來獲取數據,loading狀態將被設置爲true。一旦請求成功獲取到數據,loading狀態將會再次被設置爲false

用`React Hooks`處理錯誤

怎樣用React hook來爲數據獲取進行錯誤處理呢?error只是用state hook初始化的另一個state,一旦有一個錯誤狀態,App組件會爲用戶渲染錯誤頁面。當使用async/await的時候,使用try/catch來進行錯誤處理是常見的,你能夠在effect裏來作這件事。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const FetchData: React.FC = () => {
  const [data, setData] = useState<IData>({ 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);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData({ hits: result.data.hits });
      } catch (e) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData().then();
  }, [url]);
  return (
    <Card bordered={false}>
      <Input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
      {isError ?
        <div>something went wrong...</div>
        :
        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
          <List.Item>
            <a href={item.url}>{item.title}</a>
          </List.Item>
        )}/>}
    </Card>
  );
};
export default FetchData;
複製代碼

每次hooks再次運行的時候錯誤狀態將會被重置。這是有用的,由於在請求失敗後用戶可能想要再次嘗試,這個時候應該重置錯誤。爲了強行製造一個錯誤你能夠將url改成某些無效的值,而後檢查錯誤信息是否展現。

在`React`中從表單獲取數據

怎樣用一個合適的表單來獲取數據呢?目前爲止,咱們只有輸入框和按鈕進行組合。一旦你想要引入更多的input元素,你可能想要用一個form元素來包裹它們。此外,form也可讓你用回車鍵來觸發search按鈕。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const App: React.FC = () => {
  const [data, setData] = useState<IData>({ 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);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData({ hits: result.data.hits });
      } catch (e) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData().then();
  }, [url]);
  return (
    <Card bordered={false}>
      <form onSubmit={(e) => {
        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
      }}>
        <Input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <Button htmlType="submit">Search</Button>
      </form>
      {isError ?
        <div>something went wrong...</div>
        :
        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
          <List.Item>
            <a href={item.url}>{item.title}</a>
          </List.Item>
        )}/>}
    </Card>
  );
};
export default App;
複製代碼

可是如今當點擊提交按鈕的時候,瀏覽器將會從新加載,這是在提交一個表單時瀏覽器的默認行爲。爲了阻止默認行爲,咱們要在React事件內調用一個函數。在React類組件中你也能夠這樣作。

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';

const FetchData: React.FC = () => {
  const [data, setData] = useState<IData>({ 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);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData({ hits: result.data.hits });
      } catch (e) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData().then();
  }, [url]);
  return (
    <Card bordered={false}>
      <form onSubmit={(e) => {
        e.preventDefault();
        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
      }}>
        <Input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <Button htmlType="submit">Search</Button>
      </form>
      {isError ?
        <div>something went wrong...</div>
        :
        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
          <List.Item>
            <a href={item.url}>{item.title}</a>
          </List.Item>
        )}/>}
    </Card>
  );
};
export default FetchData;
複製代碼

如今,當你點擊提交按鈕後瀏覽器再也不會從新加載。它和以前同樣工做,但這時是一個表單而不是原生輸入框和按鈕的組合,你也能夠按下你鍵盤上的回車鍵來提交表單內容。

自定義數據獲取`Hook`

爲了爲獲取數據提取自定義hook,除了屬於輸入框的query狀態,移動包括loading展現和錯誤處理在內全部屬於數據獲取的代碼到它本身的函數中。固然,也要確保從函數中返回全部在App組件中所用到的必須的變量。

初始狀態也能變得通用,只須要簡單地將它傳給新的自定義hook

import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import axios from 'axios';

interface IResult<T> {
  data: T;
  isLoading: boolean;
  isError: boolean;
}

const useHackerNewsApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
  const [data, setData] = useState<T>(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 (e) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData().then();
  }, [url]);
  return [{ data, isLoading, isError }, setUrl];
};

export default useHackerNewsApi;
複製代碼

Tip: tsx中箭頭函數定義泛型的語法

如今,你的新hook能夠在App組件中再次使用:

import React, { useState } from 'react';
import { Button, Card, Input, List } from 'antd';
import useHackerNewsApi from '@/views/fetchData/useHackerNewsApi';
import { IData } from '@/responseTypes';

const FetchData: React.FC = () => {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, setUrl] = useHackerNewsApi<IData>({ hits: [] }, 'https://hn.algolia.com/api/v1/search?query=redux');
  return (
    <Card bordered={false}>
      <form onSubmit={(e) => {
        e.preventDefault();
        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
      }}>
        <Input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <Button htmlType="submit">Search</Button>
      </form>
      {isError ?
        <div>something went wrong...</div>
        :
        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
          <List.Item>
            <a href={item.url}>{item.title}</a>
          </List.Item>
        )}/>}
    </Card>
  );
};
export default FetchData;
複製代碼

這就是用一個自定義hook來獲取數據。hook自己並不知道關於API的任何內容,它接收全部來自外部的參數,而且只管理必要的狀態,好比:data,loading,error。它就像自定義請求數據hook同樣爲使用到它的組件發起請求而且返回數據。

使用`reducer hook`獲取數據

到目前爲止,咱們已經使用多個state hook來管理咱們的數據獲取狀態data,加載狀態loading和錯誤狀態error。然而,用單獨的state hook管理的全部這些狀態都應該屬於同一類,由於它們關心相同的問題。正如你看到的,他們都在數據獲取函數中被用到。它們是一個接一個地使用的(好比:setIsError,setIsLoading),這能夠很好的代表它們是在一塊兒的。讓咱們將三個狀態所有與Reducer Hook結合使用。

Reducer Hook爲咱們返回一個state對象以及一個更改state對象的函數。這個函數叫作派發(dispatch)函數,它接收一個擁有type和可選的payloadaction做爲參數。全部的這些信息用來從以前的狀態以及action的可選的payloadtype來提取一個新的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);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoadingfalse,
    isErrorfalse,
    data: initialData,
  });
  ...
};
複製代碼

Reducer hook接受reducer函數和一個初始的state對象做爲參數。在咱們的例子中,data,loadingerror狀態的初始參數是不會變化的,可是它們被聚合到了一個狀態對象,經過一個reducer hook代替每一個單獨的state hook

const dataFetchReducer = (state, action) => {
  ...
};
const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoadingfalse,
    isErrorfalse,
    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函數哪個狀態轉換須要被應用,payload被用來從reducer提取新的state。最終,咱們只有三種狀態轉換:初始化數據獲取過程、數據獲取結果成功的通知、數據獲取結果異常的通知。

在自定義hook的最後,state像以前同樣被返回,由於咱們有一個state對象而再也不是幾個獨立的state。經過這種方式,調用useDataApi自定義hook的組件仍然可使用dataisLoadingisError

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoadingfalse,
    isErrorfalse,
    data: initialData,
  });
  ...
  return [state, setUrl];
};
複製代碼

最後很重要的一點,咱們少了對reducer函數的實現。它須要處理三種不一樣的狀態轉換,分別是FETCH_INIT,FETCH_SUCCESSFETCH_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和執行dispatch時傳入的action。目前爲止,在switch case語句中每個狀態轉換隻返回了以前的state。展開語法用來保證state對象不可變(意味着state永遠不能直接改變)以實施最佳實踐。如今讓咱們覆蓋一些當前狀態的返回屬性來改變每一次狀態變換的state

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoadingtrue,
        isErrorfalse
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoadingfalse,
        isErrorfalse,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoadingfalse,
        isErrortrue,
      };
    default:
      throw new Error();
  }
};
複製代碼

如今,經過actiontype決定的每一次狀態轉換,將會基於以前的狀態和可選的payload返回一個新的狀態。好比,在一次成功請求的狀況下,payload被用來設置新的狀態對象的data屬性。

總之,Reducer Hook確保這部分狀態管理用它本身的邏輯來封裝。經過提供action types和可選的payload,你將總會用一個可預測的狀態改變來更新state。此外,你將永遠不會遇到無效的狀態。例如,在這以前,isLoadingisError狀態可能會被意外的都設置爲true。這種狀況下UI應該顯示什麼呢?如今,經過reducer函數定義的每一個狀態變換都會指向一個有效的state對象。

譯者注:全部的自定義hook的源碼以下

import { Dispatch, Reducer, SetStateAction, useEffect, useReducer, useState } from 'react';
import axios from 'axios';

interface IResult<T = any> {
  data: T;
  isLoading: boolean;
  isError: boolean;
}

type IAction<T = any> = {
  type'FETCH_INIT';
} | {
  type'FETCH_SUCCESS';
  payload: T
} | {
  type'FETCH_FAILURE';
}
const dataFetchReducer = <T extends any> (state: IResult<T>, action: IAction<T>): IResult<T> => {
  switch (action.type) {
    case 'FETCH_INIT':
      return { ...state, isLoading: true };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        data: action.payload,
        isLoading: false,
        isError: false
      };
    case 'FETCH_FAILURE':
      return { ...state, isError: true };
    default:
      throw new Error();
  }
};

const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
  const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {
    data: initialData,
    isError: false,
    isLoading: false
  });
  const [url, setUrl] = useState(initialUrl);
  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (e) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };
    fetchData().then();
  }, [url]);
  return [state, setUrl];
};

export { useDataApi };
複製代碼

在組件中這樣使用:

import React, { useState } from 'react';
import { Button, Card, Input, List } from 'antd';
import { useDataApi } from '@/views/fetchData/useHackerNewsApi';
import { IData } from '@/responseTypes';

const FetchData: React.FC = () => {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, setUrl] = useDataApi<IData>({ hits: [] }, 'https://hn.algolia.com/api/v1/search?query=redux');
  return (
    <Card bordered={false}>
      <form onSubmit={(e) => {
        e.preventDefault();
        setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
      }}>
        <Input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <Button htmlType="submit">Search</Button>
      </form>
      {isError ?
        <div>something went wrong...</div>
        :
        <List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
          <List.Item>
            <a href={item.url}>{item.title}</a>
          </List.Item>
        )}/>}
    </Card>
  );
};
export default FetchData;
複製代碼

在`effect hook`中終止數據獲取

即便組件已經卸載(好比因爲使用React Router導航離開當前組件)可是組件的狀態仍是會被設置,這在React中是一個常見的問題。我以前在這裏寫過關於這個問題的文章,它描述了在各類場景下如何阻止卸載組件設置狀態。讓咱們看一下如何在獲取數據的自定義hook中阻止設置狀態:

const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
  const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {
    data: initialData,
    isError: false,
    isLoading: false
  });
  const [url, setUrl] = useState(initialUrl);
  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 (e) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
    fetchData().then();
    return () => {
      didCancel = true;
    };
  }, [url]);
  return [state, setUrl];
};
複製代碼

在組件卸載的時候,每個Effect Hook所對應的清理函數將會運行。清理函數是一個從hook裏返回的函數。在咱們的例子中,咱們使用一個叫作didCancel的布爾值做爲標識來讓數據獲取邏輯知道組件的狀態(掛載/卸載)。若是組件已經卸載了,應該將標記設置爲true,從而阻止在異步解析數據以後設置組件的狀態。

注意:實際上數據獲取並無被終止(終止數據獲取能夠用Axios Cancellation來實現),可是已經掛載的組件再也不執行狀態轉換。由於Axios Cancellation在我眼中不是最好的API,這個布爾值也能作阻止組件設置狀態的工做。

相關文章
相關標籤/搜索