跳至主要内容

[react] React 使用 RTK Query 來獲取 API 資料

安裝

npm install react-redux @reduxjs/toolkit

資料夾與檔案結構

程式碼連結

src
| App.js
| index.js
| ...
|
└─── components
│ │ Post.js
| | TestAPI.js

└─── store
│ │ index.js
│ │
│ └─── api
│ │ │ apiSlice.js

Image

說明

這篇文章 的最後有提到,如果要使用 Redux Toolkit 的 slice 實作 async await 去取得 API 資料的話,會需要自訂一個 action creator,在 action creator 裡面還需要使用到 dispatch,這是一件不太方便的事情。

所以 Redux Toolkit 有提供一套解決方法給我們使用,叫做 Redux Toolkit Query,以下簡稱 RTK Query。

RTK Query 是專門用來取得 API 資料的一個套件,它提供了以下幾個優點:

  • 能夠追蹤 API 的載入狀態,以便顯示 Loading 畫面。
  • 要是兩個以上的 Component 呼叫了同樣的 API,並不會發送兩次 Request 出去,只會送一次 Request。
  • 在使用者與畫面互動時,能夠將取得到的 API 資料使用 cache 暫存,以此保持良好的使用者體驗。

ApiProvider

先介紹第一種 RTK Query 的使用方法,要使用 RTK Query 一樣要引入 Provider 包住我們的整個 React Application,而 RTK 有提供另外一個 Provider 給 RTK Query 使用,叫做 ApiProvider。

ApiProvder 不能夠與 store 的 Provider 一同使用,會有問題,官方也有特別說明這點。

危險

Using ApiProvider together with an existing Redux store will cause them to conflict with each other.

App.js
import "./App.css";import { Provider } from "react-redux";import store from "./store";import { ApiProvider } from "@reduxjs/toolkit/dist/query/react";function App() {  return (    //錯誤的程式碼,ApiProvider 不能夠與 Provide r一同使用。    <ApiProvider>      <Provider store={store}>        <div className="App">...</div>      </Provider>    </ApiProvider>  );}export default App;

但這是有解決辦法的,我們待會介紹到第二種使用方法的時候會說明。

現在來使用 RTK Query 撰寫抓取 API 資料的程式碼吧,範例程式碼會使用 JSONPlaceholder 提供的 posts API 來實作 RTK Query。

https://jsonplaceholder.typicode.com/posts

先使用 createApi 來建立我們的 API 服務,在 createApi 內需要填入 3 個參數:

  • reducerPath : reducer 的 path 名稱,通常會設定跟 API 服務一樣的名稱。
  • baseQuery : 使用 fetchBaseQuery 後,可以設定 API 的 baseUrl。
  • endPoints : 定義要呼叫的 API Function 名稱,而 RTK Query 會自動將裡面定義的 Function 處理成 Hook 讓我們去使用。
apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";export const postsApi = createApi({  reducerPath: "postsApi",  baseQuery: fetchBaseQuery({    baseUrl: "https://jsonplaceholder.typicode.com/",  }),  endpoints: (builder) => ({    getAllPosts: builder.query({      /* 因為有設定baseUrl的關係,不用填寫完整的 API 網址。      當呼叫該Function時,是去 https://jsonplaceholder.typicode.com/posts 取得資料 */      query: () => "posts", // https://jsonplaceholder.typicode.com/posts    }),  }),});// RTK Query 會自動將 endPoints 內定義的 Function 輸出成 Hook。// 格式為 use + 定義的 Function 名稱 + Queryexport const { useGetAllPostsQuery } = postsApi;
備註

使用 query 發送的是 GET Request,如果要發送 Post 的話要更改為 Mutation。

接著在 App.js 去引入 ApiProvider,而 ApiProvider 還需要提供一個 prop,叫做 api,這邊就直接將我們剛剛定義的 API 服務傳入即可。

Post 和 TestAPI 為待會測試 RTK Query 是否能夠成功執行的 Component。

index.js
import "./App.css";import Post from "./components/Post";import TestAPI from "./components/TestAPI";import { ApiProvider } from "@reduxjs/toolkit/dist/query/react";import { postsApi } from "./store/api/apiSlice";function App() {  return (    //錯誤的程式碼,ApiProvider不能夠與Provider一同使用。    <ApiProvider api={postsApi}>      <div className="App">        <Post />        <TestAPI />      </div>    </ApiProvider>  );}export default App;

現在來 Post Component 內測試是否能使用 RTK Query 去獲取 JSONPlaceholder 的 API 資料。

只要將 RTK Query 提供給我們的 Hook 引入並直接使用即可。

Post.js
import React from "react";import { useGetAllPostsQuery } from "../store/api/apiSlice";const Post = () => {  //將資料解構出來  const { data, isLoading } = useGetAllPostsQuery();  //判斷是否正在載入API資料  if (isLoading) {    return <h1>Loading...</h1>;  }  console.log("Posts", data);  return <h1>Post</h1>;};export default Post;

接著打開 console,確認一下是否有取得到 API 資料。

可以看到我們成功使用 RTK Query 取得到 API 資料了。

Image

在 TestAPI Component 也去使用 useGetAllPostsQuery Hook。

TestAPI.js
import React from "react";import { useGetAllPostsQuery } from "../store/api/apiSlice";const TestAPI = () => {  //將資料解構出來  const { data, isLoading } = useGetAllPostsQuery();  //判斷是否正在載入API資料  if (isLoading) {    return <h1>Loading...</h1>;  }  console.log("TestAPI", data);  return <h1>TestAPI</h1>;};export default TestAPI;

Image

在正常的情況下,我們在兩個 Component 都去呼叫了 JSONPlaceholder 的 API,所以應該會發送兩次的 Request 出去。

但實際上我們只發送了一次 Request 就取得了資料,這也是前面提到的 RTK Query 的優點,要是多個 Component 發送請求給同一個 API,RTK Query 只會發送一次 Request。

Image

Provider

而前面有提到,store 使用的 Provider 並不能與 ApiProvider 一同使用,這邊官方有提供相對應的解決方法,只要在 configureStore 內去定義 API 服務的 reducerPath 和 reducer 即可。

store/index.js
import { configureStore } from "@reduxjs/toolkit";import { postsApi } from "./api/apiSlice";const store = configureStore({  reducer: {    [postsApi.reducerPath]: postsApi.reducer,    // ...其他slice  },  //使用 api middleware 可以啟用 caching、invalidation、polling 功能。  middleware: (getDefaultMiddleware) =>    getDefaultMiddleware().concat(postsApi.middleware),});export default store;
App.js
import "./App.css";import Post from "./components/Post";import TestAPI from "./components/TestAPI";import { Provider } from "react-redux";import store from "./store";// import { ApiProvider } from "@reduxjs/toolkit/dist/query/react";// import { postsApi } from "./store/api/apiSlice";function App() {  return (    <Provider store={store}>      <div className="App">        <Post />        <TestAPI />      </div>    </Provider>  );}export default App;

現在再次回到網頁重新整理,一樣可以看到我們的 API 資料有成功的取回。

Image

補充

如果 API 需要帶入參數來取得資料的話,可以使用以下的做法:

apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";export const postsApi = createApi({  reducerPath: "postsApi",  baseQuery: fetchBaseQuery({    baseUrl: "https://jsonplaceholder.typicode.com/",  }),  endpoints: (builder) => ({    getAllPosts: builder.query({      query: () => "posts", // https://jsonplaceholder.typicode.com/posts    }),    getPostById: builder.query({      query: (id) => `posts?id=${id}`, // https://jsonplaceholder.typicode.com/posts?id=1    }),  }),});export const { useGetAllPostsQuery, useGetPostByIdQuery } = postsApi;
Post.js
import React from "react";import { useGetPostByIdQuery } from "../store/api/apiSlice";const Post = () => {  //取得id為1的文章  const { data, isLoading } = useGetPostByIdQuery(1);  if (isLoading) {    return <h1>Loading...</h1>;  }  console.log("Posts", data);  return <h1>Post</h1>;};export default Post;

Image