[譯] 如何使用 React hooks 獲取 api 接口數據

原文地址:robinwieruch 全文使用意譯,不是重要的我就沒有翻譯了前端

在本教程中,我想向你展現如何使用 state 和 effect 鉤子在React中獲取數據。 你還將實現自定義的 hooks 來獲取數據,能夠在應用程序的任何位置重用,也能夠做爲獨立節點包在npm上發佈。react

若是你對 React 的新功能一無所知,能夠查看 React hooks 的相關 api 介紹。若是你想查看完整的如何使用 React Hooks 獲取數據的項目代碼,能夠查看 github 的倉庫ios

若是你只是想用 React Hooks 進行數據的獲取,直接 npm i use-data-api並根據文檔進行操做。若是你使用他,別忘記給我個star 哦~git

注意:未來,React Hooks 不適用於 React 中獲取數據。一個名爲Suspense的功能將負責它。如下演練是瞭解React中有關 state 和 Effect hooks 的更多信息的好方法。github

使用 React hooks 獲取數據

若是您不熟悉React中的數據提取,請查看我在React文章中提取的大量數據。 它將引導您完成使用React類組件的數據獲取,如何使用Render Prop 組件和高階組件來複用這些數據,以及它如何處理錯誤以及 loading 的。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 的 hook。他是來負責管理咱們這個 data 的狀態的。userState 中的第一個值是data 的初始值。其實就是個解構賦值。redux

這裏咱們使用 axios 來獲取數據,固然,你也可使用別的開源庫。axios

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 的 effect hook 來獲取數據。而且使用 useState 中的 setData 來更新組件狀態。api

可是如上代碼運行的時候,你會發現一個特別煩人的循環問題。effect hook 的觸發不只僅是在組件第一次加載的時候,還有在每一次更新的時候也會觸發。因爲咱們在獲取到數據後就進行設置了組件狀態,而後又觸發了 effect hook。因此就會出現死循環。很顯然,這是一個 bug!咱們只想在組件第一次加載的時候獲取數據 ,這也就是爲何你能夠提供一個空數組做爲 useEffect 的第二個參數以免在組件更新的時候也觸它。固然,這樣的話,也就是在組件加載的時候觸發。數組

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;
複製代碼

第二個參數能夠用來定義 hook 所依賴的全部變量(在這個數組中),若是其中一個變量發生變化,則就會觸發這個 hook 的運行。若是傳遞的是一個空數組,則僅僅在第一次加載的時候運行。

是否是感受 ,幹了shouldComponentUpdate 的事情

這裏還有一個陷阱。在這個代碼裏面,咱們使用 async/await 去獲取第三方的 API 的接口數據,根據文檔,每個 async 都會返回一個 promise:async 函數聲明定義了一個異步函數,它返回一個 AsyncFunction 對象。異步函數是經過事件循環異步操做的函數,使用隱式的 Promise 返回結果然而,effect hook 不該該返回任何內容,或者清除功能。這也就是爲啥你看到這個警告:07:41:22.910 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(
          '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 來獲取 API 數據。可是,若是你對錯誤處理、loading、如何觸發從表單中獲取數據或者如何實現可重用的數據獲取的鉤子。請繼續閱讀。

如何自動或者手動的觸發 hook? (How to trigger a hook programmatically/manually?)

目前咱們已經經過組件第一次加載的時候獲取了接口數據。可是,如何可以經過輸入的字段來告訴 api 接口我對那個主題感興趣呢?(就是怎麼給接口傳數據。這裏原文說的有點囉嗦(還有 redux 關鍵字來混淆視聽),我直接上代碼吧)...

...

  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 hook 的觸發不依賴任何變量,所以只在組件第一次加載的時候觸發。因此這裏咱們但願當 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]);

  return (
    ...
  );
}

export default App;
複製代碼

如上,咱們只是把 query做爲第二個參數傳遞給了 effect hook,這樣的話,每當 query 改變的時候就會觸發搜索。可是,這樣就會出現了另外一個問題:每一次的query 的字段變更都會觸發搜索。如何提供一個按鈕來觸發請求呢?

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>
  );
}
複製代碼

搜索的狀態設置爲組件的初始化狀態,組件加載的時候就要觸發搜索,相似的查詢和搜索狀態易形成混淆,爲何不把實際的 URL 設置爲狀態而不是搜索狀態呢?

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>
  );
}
複製代碼

這是一個使用 effect hook 來獲取數據的一個例子,你能夠決定 effect hook 因此依賴的狀態。一旦你點擊或者其餘的什麼操做 setState 了,那麼 effect hook 就會運行。可是這個例子中,只有當你的 url 發生變化了,纔會再次去獲取數據。

在 Effect Hook 中使用 Loading(Loading Indicator with React Hooks)

這裏讓咱們來給程序添加一個 loading(加載器),這裏須要另外一個 state

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);

  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 ? (
        <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;

複製代碼

代碼比較簡單,不解釋了

使用 Effect Hook 添加錯誤處理(Error Handling with React Hooks)

如何在 Effect Hook 中作一些錯誤處理呢?錯誤僅僅是一個 state ,一旦程序出現了 error state,則組件須要去渲染一些feedback 給用戶。當咱們使用 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);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      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>

      {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;
複製代碼

每一次 effect hook 運行的時候都須要重置一下 error state,這是很是有必要的。由於用戶可能想再發生錯誤的時候想再次嘗試一下。

說白了,界面給用戶反饋更加的友好

使用 React 中 Form 表單獲取數據(Fetching Data with Forms and 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>
  );
}
複製代碼

爲了防止瀏覽器的 reload,咱們這裏加了一個event.preventDefalut(),而後別的操做就是正常表單的操做了

自定義獲取數據的 hook(Custom Data Fetching Hook)

其實就是請求的封裝

爲了可以提取自定義的請求 hook,除了屬於輸入框的 query 字段,別的包括 loading 加載器、錯誤處理函數都要包括在內。固然,你須要確保 App Component 所需的全部字段在你自定義的 hook 中都有返回

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];
}
複製代碼

如今,咱們能夠將你的新 hook 繼續放到組件中使用

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>
  );
}
複製代碼

一般咱們須要一個初始狀態。將它簡單的傳遞給自定義 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]);

  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;
複製代碼

如上,就是咱們使用自定義 hook 來獲取數據,該 hook 自己對 API 一無所知,它從外部接受全部的參數,可是僅管理重要的字段,好比 data、loading、error handler 等。它執行請求而且返回組件所須要的所有數據。

用於數據獲取的 Reducer Hook(Reducer Hook for Data Fetching)

目前爲止,咱們使用各類 state hook 來管理數據、loading、error handler 等。然而,全部的這些狀態,經過他們本身的狀態管理,都屬於同一個總體,由於他們所關心的數據狀態都是請求相關的。正如你所看到的,他們都在 fetch 函數中使用。他們屬於同一類型的另外一個很好的表現就是在函數中,他們是一個接着一個被調用的(好比:setIsError、setIsLoading)。讓咱們用一個 Reducer Hook 來將這三個狀態結合起來!

一個 Reducer Hook 返回一個狀態對象和一個改變狀態對象的函數。這個函數就是 dispatch function:帶有一個 type 和參數的 action。

其實這些概念跟 redux 一毛同樣

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函數和初始狀態對象做爲參數。 在咱們的例子中,數據,加載和錯誤狀態的初始狀態的參數沒有改變,但它們已經聚合到一個由 reducer hook 而不是單個state hook 管理的狀態對象。

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]);

  ...
};
複製代碼

如今,在獲取數據的時候,可使用 dispathc function 來給reducer傳遞參數。使用dispatch函數發送的對象具備必需的type屬性和可選的payload屬性。該類型告訴reducer功能須要應用哪一個狀態轉換,而且reducer能夠另外使用有效負載來提取新狀態。畢竟,咱們只有三個狀態轉換:初始化提取過程,通知成功的數據提取結果,並通知錯誤的數據提取結果。

在咱們自定義的 hook 中,state 像之前同樣返回。可是由於咱們有一個狀態對象而不是獨立狀態。 這樣,調用useDataApi自定義鉤子的人仍然能夠訪問數據,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];
};
複製代碼

最後還有咱們 reducer 函數的實現。它須要做用於三個不一樣的狀態轉換,稱爲FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。 每一個狀態轉換都須要返回一個新的狀態對象。 讓咱們看看如何使用switch case語句實現它:

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 都有對應的處理,而且返回一個新的 state。

總之,Reducer Hook確保狀態管理的這一部分用本身的邏輯封裝。此外,你永遠不會遇到無效狀態。例如,之前可能會意外地將isLoading和isError狀態設置爲true。 在這種狀況下,UI應該顯示什麼?如今,reducer函數定義的每一個狀態轉換都會致使一個有效的狀態對象。

在 Effect Hook 中 停止數據請求(Abort Data Fetching in Effect Hook)

React中的一個常見問題是,即便組件已經卸載(例如因爲使用React Router導航),也會設置組件狀態。我以前已經在這裏寫過關於這個問題的文章,它描述瞭如何防止在各類場景中爲未加載的組件中設置狀態。 讓咱們看看咱們如何阻止在數據提取的自定義鉤子中設置狀態:

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]);

  return [state, setUrl];
};
複製代碼

每個 Effect Hook 都自帶一個清理功能。該功能在組件卸載時運行。清理功能是 hook 返回的一個功能。在咱們的例子中,咱們使用一個名爲 didCancel 的 boolean 來標識組件的狀態。若是組件已卸載,則該標誌應設置爲true,這將致使在最終異步解析數據提取後阻止設置組件狀態。

注意:實際上不會停止數據獲取 - 這能夠經過Axios Cancellation實現 - 可是對於 unmounted 的組件再也不執行狀態轉換。 因爲Axios Cancellation在我看來並非最好的API,所以這個防止設置狀態的布爾標誌也能完成這項工做。

學習交流

關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流呀~~

相關文章
相關標籤/搜索