翻译: 如何使用React Hook 获取数据

翻译自 How to fetch data with React Hooks, 不好意思, 翻译的比较差, 主要看代码. 我也是英语初学者, 主要是理解 代码预览

在这个教程, 我想要向您展示如何使用State 和 Effect钩子在 React Hook来获取数据, 我们将使用广为人知的 Hacker News API 从科技界获取热门文章, 您还将实现数据获取的自定义钩子可以在任何地方重用.

用React Hook 获取数据

如何你不熟悉在React中如何获取数据,在本文中, 我想向您展示函数组件中的React Hook 的所有内容

import React from 'react'
import { useState } from 'react'

const App = () => {
  const [data, setDate] = useState({ hits: [] })
  return (
    
  )
}

export default App

这个App组件展示了list 的每一个 item(hits = Hacker News articles), 这个state 和 updateState函数来自被叫做 useState 的钩子, 它负责管理我们将为 App 组件数据获取的本地状态. 初始的 state 是一个表示对象的 hits 空列表, 没有人为此设置任何 state

我们将使用 axios 来获取数据, 你可以通过 npm install axios 来安装依赖, 然后实现 effect 钩子获取数据

import * as React from "react";
import { 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 (
    
  );
}

export default App;

上面叫做 useEffect 的钩子被用于从 axios 获取数据, 并用 setData 函数来更新数据设置组件的当前状态, Promise 使用 async/await 实现

然而, 当你运行你的程序, 你就会陷入一个恶心循环, effect 钩子在组件挂载时运行也在组件更新时运行, 因为我们每次获取数据之后都要设置状态, 所有组件会更新, effect 会再次运行, 会一次次的请求数据, 这是一个 bug, 需要我们避免. 我们只想在组件挂载时获取数据

import * as React from "react";
import { 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 (
    
  );
}

export default App;

第二个参数可以用来定义 useEffect钩子依赖的所有的变量(在数组中分配), 如果其中一个变量改变, useEffect会再次运行, 如果数组的变量是空的, 更新组件时根本不会运行, 因为它不需要观察任何变量


如何手动/以程序触发一个钩子

很好, 一旦组件挂载, 我们就可以获取数据了, 但是如何通过使用一个 input 来告诉 API 我们对哪个主题感兴趣呢? query=redux 是一个默认的查询, 但是关于 React 的主题呢? 让我们实现一个 input 元素, 能够让别人来获取除了 Redux 之外的主题, 因此, 为输入框引入一个新的状态

import * as React from 'react';
import { useState, useEffect, Fragment } 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 (
    
       setQuery(event.target.value)}
      />
      
    
  );
}

export default App;

目前,两个 state 彼此之间是互相独立的, 但现在你希望他们耦合到只需要输入字段查询指定的文章, 所以组件一旦挂载, 就应该按照查询条件获取所有文章

// ...

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=${query}` // 动态参数
      );
      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    // ...
  );
}

少了一点: 当你尝试输入一些值在 input 中, 没有其他数据获取后, 触发 effect 的效果, 这是因为你提供了 空数组作为第二个参数, 这种效果不依赖于任何变量, 因此只在组件挂载时才会触发, 然而, 现在这个渲染效果依赖 query 的变化, 数据请求应该再次触发

// ...
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=${query}`
      );
      setData(result.data);
    };

    fetchData();
  }, [query]); // 添加 query 依赖

  return (
    // ...  
  );
}

一旦你更改了输入框的值, 就会重新请求数据, 但是这会产生另外一个问题: 在每一个字符输入到输入框中, 就会产生一次请求. 如何 提供一个按钮 来触发请求, 手动触发钩子?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      );
      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    
       setQuery(event.target.value)}
      />
      

      
    
  );
}

现在, 让 effect 依赖于 search state, 而不是随着字符的变化的每一次查询, 一旦用户点击 button, 新的搜索状态应该手动触发 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(
        `https://hn.algolia.com/api/v1/search?query=${search}` // 改变 search
      );
      setData(result.data);
    };

    fetchData();
  }, [search]); // 切换依赖

  return (
    // ...
  );
}

搜索状态的初始状态也被设置为和查询状态相同的状态, 因为组件也在挂载时获取数据, 因此结果应该显示在输入框内, 具有形似的查询和搜索状态令人有点困惑, 为什么我们不设置一个实际的 URL 替代 search state

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'
  ); // 替换search

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);
      setData(result.data);
    };

    fetchData();
  }, [url]); // 更换依赖项

  return (
    
       setQuery(event.target.value)}
      />
      

      
    
  );
}

你可以决定 effect 效果 哪一个 state 依赖, 一旦在点击或其他副作用中设置此状态, 这个effect会再次渲染, 在上面代码的情况下, 如果 url 变化, 会再次请求 api


给 Hook 加上 Loading

让我们为数据获取加入一个 loading, 这仅仅是加一个新的 state, 由 state hook 管理,

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

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const result = await axios(url);
      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    
       setQuery(event.target.value)}
      />
      

      {isLoading ? (
        
Loading....
) : ( )}
); }

上面代码中, 一旦开始数据请求, loading = true, 数据请求完成, laoding = false


错误处理

如何使用钩子对数据获取的错误处理? 这个错误是另一个状态初始化的 state hook

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 (
    
       setQuery(event.target.value)}
      />
      

      {isError && 
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); }

使用 form 来获取数据

如何正确的使用表单来获取数据? 到目前为止, 我们只是组合 inputbutton, 一旦你引入更多的 input 元素, 你也许想要使用 form 元素来包裹它们

function App() {
  // ...
  return (
    
      
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`) } > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); }

但是现在点击 submit button 浏览器会重新加载, 因为这是提交表单的浏览器原生行为, 为了阻止默认行为, 我们可以调用React事件

function App() {
  // ...
  return (
    
      
{ setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`) event.preventDefault() // 阻止默认事件 }} > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); }

现在当你点击按钮时, 浏览器不会重新加载了, 它和以前一样工作, 但是现在是 form 而不是纯粹的 input 和 button 组合了, 你也可以用键盘上的回车键


自定以数据获取钩子

为了提取一个请求数据的自定义hook, 移动属于数据获取的所有数据, 除了属于 input 的查询字段, 但是包含 loading/error state 至自己的函数, 同样确保 return 所有的变量在 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");
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();

  return (
    
      
{ doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`); event.preventDefault(); }} > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); }

初始状态也是通用的, 将它简单传递到新的钩子, 设置两个参数

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 (
    
      
{ doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`); event.preventDefault(); }} > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); }

这就是用自定义钩子获取数据的过程, 钩子本身对 API 一无所知, 它从外部接受所有参数并且只管理必要的状态例如: data loading error


Reducer Hook 数据请求

到目前为止, 我们使用不同的状态钩子来管理数据的数据获取状态, 如你所见, 他们都在数据获取函数中使用, 让我们用一个 Reducer Hook 来把他们组合起来

一个 Reducer Hook 返回给我们一个状态对象和一个修改状态对象的函数 function -> dispatch function -> 一个type 和一个可选的 payload, 我们看一下 reducer 是如何工作的.


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函数和一个初始化的state对象作为参数, 在我们的代码中, 它们被 reducer 一起管理, 而不是 state 一个个的管理


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 function, 使用 dispatch 派发具有 type 类型和 可选的payload属性, type类型告诉 reducer 需要应用那个状态转换, 并且 reducer 可以使用 payload来提取新的状态, 我们只有三个状态转换

在自定义hook的最后, 状态和以前一样返回, 我们有一个状态对象而不是一个独立的状态, 来完善 reducer

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

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

  return [state, setUrl];
};

现在每个状态的过渡, 由 type 的类型决定, 根据以前的状态和可选的载荷返回一个新状态

最后, Reducer Hook 确保状态管理封装了自己的逻辑, 通过提供操作类型和可选的 payload, 你总是会得到一个可预测的变化, 另外, 你永远不会进入无效状态, 看一下最终代码

import * as React from "react";
import { useState, useEffect, useReducer, Fragment } from "react";
import axios from "axios";

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

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

  return [state, 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 (
    
      
{ doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`); event.preventDefault(); }} > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); } export default App;

终止数据请求在 effect hook

React中的一个常见问题是即使组件已经卸载, 组件状态也会被设置, 让我们看看如何放置在自定义钩子中设置状态以获取数据

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 都带有清理功能, 该功能在组件卸载时运行, 清理函数是从钩子的 return 一个函数, 在我们的例子中, 我们使用一个叫做 didCancelboolean 值来让我们的数据获取逻辑了解组件的状态(mounted/unmounted)

Note: 实际上不是数据获取被中止 -- 这可以通过 Axios Cancellation 实现 -- 但是对于 unmounted 组件不在执行状态转换, 由于 axios cancel 在我看来并不是最好的 API, 所以设置这个 boolean 也可以完成这个工作


你已经了解了如何在 React 中使用 state 和 effect 的钩子来获取数据, 谢谢, 最终代码贴一下

import * as React from "react";
import { useState, useEffect, useReducer, Fragment } from "react";
import axios from "axios";

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

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

function App() {
  const [query, setQuery] = useState("redux");
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    "https://hn.algolia.com/api/v1/search?query=redux",
    {
      hits: []
    }
  );

  return (
    
      
{ doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`); event.preventDefault(); }} > setQuery(event.target.value)} />
{isError &&
Something went wrong ...
} {isLoading ? (
Loading....
) : ( )}
); } export default App;

你可能感兴趣的:(翻译: 如何使用React Hook 获取数据)