在 React Hooks 中怎样要求数据?

《在 React Hooks 中怎样要求数据?》

经由历程这个教程,我想关照你在 React 中怎样运用 state 和 effect 这两种 hooks 去要求数据。我们将运用尽人皆知的 Hacker News API 来猎取一些热点文章。你将定义属于你自身的数据要求的 Hooks ,而且可以在你一切的运用中复用,也可以宣布到 npm 。

假如你不相识 React 的这些新特征,可以检察我的另一篇文章 introduction to React Hooks。假如你想直接检察文章的示例,可以直接 checkout 这个 Github 堆栈

注重:在 React 将来的版本中,Hooks 将不会用了猎取数据,取而代之的是一种叫做
Suspense 的东西。尽管如此,下面的要领依旧是相识 state 和 effect 两种 Hooks 的好要领。

运用 React Hooks 举行数据要求

假如你没有过在 React 中举行数据要求的履历,可以浏览我的文章:How to fetch data in React。文章解说了怎样运用 Class components 猎取数据,怎样运用可重用的 Render Props Components 和 Higher Order Components ,以及怎样举行毛病处置惩罚和 loading 状况。在本文中,我想用 Function components 和 React Hooks 来重现这一切。

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 组件将展现一个列表,列表信息来自 Hacker News articles 。状况和状况更新函数将经由历程被称为 useState 的状况钩子来天生,它担任治理经由历程要求获得的 App 组件的当地状况。初始状况是一个空数组,如今没有任何地方给它设置新的状况。

我们将运用 axios 来猎取数据,固然也可以运用你熟习的要求库,或许浏览器自带的 fetch API。假如你还没有装置过 axios ,可以经由历程 npm install axios 举行装置。

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

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://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 中,经由历程 axios 从 API 中猎取数据,并运用 state hook 的更新函数,将数据存入到当地 state 中。而且运用 async/await 来剖析promise。

然则,当你运转上面的代码的时刻,你会堕入到活该的死轮回中。effect hook 在组件 mount 和 update 的时刻都邑实行。因为我们每次猎取数据后,都邑更新 state,所以组件会更新,并再次运转 effect,这会一次又一次的要求数据。很明显我们须要防备如许的bug发作,我们只想在组件 mount 的时刻要求数据。你可以在 effect hook 供应的第二个参数中,传入一个空数组,如许做可以防备组件更新的时刻实行 effect hook ,然则组件在 mount 依旧会实行它。

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

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://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 将自动运转。假如第二个参数是一个空数组,那末 hook 将不会在组件更新是运转,因为它没有监控任何的变量。

另有一个须要特别注重的点,在代码中,我们运用了 async/await 来猎取第三方 API 供应的数据。依据文档,每一个 async 函数都将返回一个隐式的 promise:

“The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. ”

“async 函数定义了一个异步函数,它返回的是一个异步函数对象,异步函数是一个经由历程事宜轮回举行操纵的函数,运用隐式的 Promise 返回终究的效果。”

然则,effect hook 应当是什么也不返回的,或许返回一个 clean up 函数的。这就是为何你会在控制台看到一个毛病信息。

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。让我们来完成一个解决方案,可以在 effect hook 中运用 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(
        'http://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 举行数据要求的小案例。然则,假如你对毛病处置惩罚、loading 态、怎样触发表单数据猎取以及怎样复用出具处置惩罚 hook 感兴趣,那我们接着往下看。

怎样手动或许自动触发一个 hook?

如今我们已可以在组件 mount 以后猎取到数据,然则,怎样运用输入框动态关照 API 挑选一个感兴趣的话题呢?可以看到之前的代码,我们默许将 “Redux” 作为查询参数(’http://hn.algolia.com/api/v1/…‘),然则我们怎样查询关于 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(
        'http://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;

如今,要求数据和查询参数两个 state 互相自力,然则我们须要像一个方法愿望他们耦合起来,只猎取输入框输入的参数指定的话题文章。经由历程以下修正,组件应当在 mount 以后根据查询猎取响应文章。

...

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 不依靠于任何的变量,所以这只会在 mount 只会触发一次。然则,如今我们须要依靠查询前提,一旦查询发送转变,数据要求就应当再次触发。

...

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;

好了,如今一旦你转变输入框内容,数据就会从新猎取。然则如今又要别的一个题目:每次输入一个新字符,就会触发 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(
        `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>
  );
}

另外,search state 的初始状况也是设置成了与 query state 雷同的状况,因为组件在 mount 的时刻会要求一次数据,此时的效果也应当是回响反映的是输入框中的搜刮前提。然则, search state 和 query state 具有相似的值,这看起来比较疑心。为何不将实在的 URL 设置到 search state 中呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://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 取决于哪一个 state。在这个案例中,假如 URL 的 state 发作转变,则再次运转该 effect 经由历程 API 从新猎取主题文章。

Loading 态 与 React Hooks

让我们在数据的加载历程当中引入一个 Loading 状况。它只是另一个由 state hook 治理的状况。Loading state 用于在 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(
    'http://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;

如今当组件处于 mount 状况或许 URL state 被修正时,挪用 effect 猎取数据,Loading 状况就会变成 true。一旦要求完成,Loading 状况就会再次被设置为 false。

毛病处置惩罚与 React Hooks

经由历程 React Hooks 举行数据要求时,怎样举行毛病处置惩罚呢? 毛病只是另一个运用 state hook 初始化的另一种状况。一旦涌现毛病状况,App 组件就可以反馈给用户。当运用 async/await 函数时,一般运用 try/catch 来举行毛病捕捉,你可以在 effect 中举行下面操纵:

...

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>
    ...
    {isError && <div>Something went wrong ...</div>}
    ...
  <Fragment>
);

effect 每次运转都邑重置 error state 的状况,这很有用,因为每次要求失利后,用户能够从新尝试,如许就可以重置毛病。为了视察代码是不是见效,你可以填写一个无用的 URL ,然后搜检毛病信息是不是会涌现。

运用表单举行数据猎取

什么才是猎取数据的准确情势呢?如今我们只要输入框和按钮举行组合,一旦引入更多的 input 元素,你能够想要运用表单来举行包装。另外表单还可以触发键盘的 “Enter” 事宜。

function App() {
  ...
  const doFetch = (evt) => {
    evt.preventDefault();
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  }
  return (
    <Fragment>
      <form
        onSubmit={ doFetch }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

自定义 hook 猎取数据

我们可以定义一个自定义的 hook,提掏出一切与数据要求相干的东西,除了输入框的 query state,除此以外另有 Loading 状况、毛病处置惩罚。还要确保返回组件中须要用到的变量。

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://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]);

  const doFetch = () => {
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  };

  return { data, isLoading, isError, doFetch };
}

如今,我们在 App 组件中运用我们的新 hook 。

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();

  return (
    <Fragment>
      ...
    </Fragment>
  );
}

接下来,在外部通报 URL 给 DoFetch 要领。

const useHackerNewsApi = () => {
  ...

  useEffect(
    ...
  );

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

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

初始的 state 也是通用的,可以经由历程参数简朴的通报到自定义的 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]);

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useDataApi(
    'http://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一窍不通,它从外部猎取参数,只治理必要的 state ,如数据、 Loading 和毛病相干的 state ,而且实行要求并将数据经由历程 hook 返回给组件。

用于数据猎取的 Reducer Hook

如今为止,我们已运用 state hooks 来治理了我们猎取到的数据数据、Loading 状况、毛病状况。然则,一切的状况都有属于自身的 state hook,然则他们又都衔接在一起,体贴的是一样的事变。如你所见,一切的它们都在数据猎取函数中被运用。它们一个接一个的被挪用(比方:setIsErrorsetIsLoading),这才是将它们衔接在一起的准确用法。让我们用一个 Reducer Hook 将这三者衔接在一起。

Reducer Hook 返回一个 state 对象和一个函数(用来转变 state 对象)。这个函数被称为分发函数(dispatch function),它分发一个 action,action 具有 type 和 payload 两个属性。一切的这些信息都在 reducer 函数中被吸收,依据之前的状况提取一个新的状况。让我们看看在代码中是怎样事情的:

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 函数和一个初始状况对象作为参数。在我们的案例中,加载的数据、Loading 状况、毛病状况都是作为初始状况参数,且不会发作转变,然则他们被聚合到一个状况对象中,由 reducer hook 治理,而不是单个 state hooks。

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 函数发送的对象具有一个必填的 type 属性和一个可选的 payload 属性。type 属性关照 reducer 函数须要转换的 state 是哪一个,还可以从 payload 中提取新的 state。在这里只要三个状况转换:初始化数据历程,关照数据要求胜利的效果,以及关照数据要求失利的效果。

在自定义 hook 的末端,state 像之前一样返回,然则因为我们一切的 state 都在一个对象中,而不再是自力的 state ,所以 state 对象举行解构返回。如许,挪用 useDataApi 自定义 hook 的人依然可以 dataisLoadingisError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

末了我们还缺乏 reducer 函数的完成。它须要处置惩罚三个差别的状况转换,分被称为 FEATCH_INITFEATCH_SUCCESSFEATCH_FAILURE。每一个状况转换都须要返回一个新的状况。让我们看看运用 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 函数可以经由历程其参数接见当前状况和 dispatch 传入的 action。到如今为止,在 switch case 语句中,每一个状况转换只返回前一个状况,析构语句用于坚持 state 对象不可变(即状况永久不会被直接变动)。如今让我们重写一些当前 state 返回的属性,以便在每次转换时变动 一些 state:

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.type决议)都返回一个基于先前 state 和可选 payload 的新状况。比方,在要求胜利的情况下,payload 用于设置新 state 对象的 data 属性。

总之,reducer hook 确保运用自身的逻辑封装状况治理的这一部份。经由历程供应 action type 和可选 payload ,总是会获得可展望的状况变动。另外,永久不会碰到无效状况。比方,之前能够会心外埠将 isLoadingisError 设置为true。在这类情况下,UI中应当显现什么? 如今,由 reducer 函数定义的每一个 state 转换都指向一个有用的 state 对象。

在 Effect Hook 中中缀数据要求

在React中,纵然组件已卸载,组件 state 依然会被被赋值,这是一个罕见的题目。我在之前的文章中写过这个题目,它形貌了怎样防备在种种场景中为未挂载组件设置状况。让我们看看在自定义 hook 中,要求数据时怎样防备设置状况:

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

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

每一个Effect Hook都带有一个clean up函数,它在组件卸载时运转。clean up 函数是 hook 返回的一个函数。在该案例中,我们运用 didCancel 变量来让 fetchData 晓得组件的状况(挂载/卸载)。假如组件确切被卸载了,则应当将标志设置为 true,从而防备在终究异步剖析数据猎取以后设置组件状况。

注重:实际上并没有中断数据猎取(不过可以经由历程Axios作废来完成),然则不再为卸载的组件实行状况转换。因为 Axios 作废在我看来并非最好的API,所以这个防备设置状况的布尔标志也可以完成这项事情。

原文链接

    原文作者:Shenfq
    原文地址: https://segmentfault.com/a/1190000018652589
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞