Best way to request unknown number of API pages in useEffect hook

不羁的心 提交于 2021-02-10 14:14:33

问题


I'm trying to write a React functional component that requests a list of people exposed across multiple pages in an API:

https://swapi.dev/api/people/?page=9

I know there are currently nine pages, but this might change in the future, so I want my component to keep requesting pages until there are no more, and to accumulate and store the people in the "results" bit of each response body. I'm trying to do this with useState and useEffect hooks.

Is there a common way of solving this problem that's generally considered the best approach? Could someone show me how to modify my existing code to get it working in the optimal way please?

My current code causes the browser to get stuck endlessly loading.

Was I wrong to try and handle the entire process in the first and only execution of useEffect? Should my useState have a dependency array that gets passed page so that useEffect executes again every time page is updated?

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

function GetAllPeople() {
  const [page, setPage] = useState(1)
  const [people, setPeople] = useState([]);
  const [gotEveryone, setGotEveryone] = useState(false)
  const [error, setError] = useState(null)

  const requestData = () => (
    fetch(`https://swapi.dev/api/people/?page=${page}`)
      .then(res => {
        if (res.ok) {
          let response = res.json()
          setPeople(people.concat(response.results))
          setPage(page + 1)
        } else if(res.status === 404) {
          setGotEveryone(true)
        } else {
          setError(res.status)
          return Promise.reject('some other error: ' + res.status)
        }
    })
  )

  useEffect(() => {
    while(gotEveryone === false) {
      if (!error) {
        requestData()
      }
    }
  })

  return (
    <div>
      <ul>
        {people.map((person, index) =>
          <li key={index}>
            {person.name}
          </li>
        )}
      </ul>
    </div>
  )
}

export default GetAllPeople;

回答1:


Unnecessary hooks.. work with api data (Recursion call)

I make a codesandbox with mock data to save api request (for development)

https://codesandbox.io/s/elastic-field-i89yu

but the main idea is here:

using next key in api response:

You can make it more elegant:

function GetAllPeople() {
  const [people, setPeople] = useState([]);

  const requestData = useCallback((url = 'https://swapi.dev/api/people/?page=1') => (
    fetch(url)
      .then(async res => {
        const response = await res.json();
        setPeople(prevState => prevState.concat(response.results));
        if (response.next) {
          requestData(response.next.replace("http://", "https://"));
        } else {
          console.log("Finish !");
        }
      }).catch(err => {
        throw err;
      })
  ), []);

  useEffect(() => {
    requestData();
  }, [requestData]);

  return (
    ...
  )
}



回答2:


Disclosure: I am the library author of suspense-service

Using React Suspense for Data Fetching, and building on the suggestion provided by Omer, you can load your results by creating a stateful service to iterate over each of the API endpoints:

const { useEffect, useState } = React;
const { createService, useServiceState } = SuspenseService;

const FetchJSON = createService((url) => (
  fetch(url).then((res) => res.json())
));

function useFetch() {
  return useServiceState(FetchJSON);
}

function People() {
  const [data, setUrl] = useFetch();
  const [people, setPeople] = useState([]);

  useEffect(() => {
    const { next, results } = data;
    if (next) setUrl(next);
    setPeople(prev => prev.concat(results));
  }, [data]);

  const list = people.map(({ name, url }) =>
    <li key={url}>
      {name}
    </li>
  );

  return (
    <div>
      <ul>
        {list}
      </ul>
    </div>
  );
}

function App() {
  return (
    <FetchJSON.Provider
      request="https://swapi.dev/api/people/?page=1"
      fallback={<p>Loading...</p>}
    >
      <People />
    </FetchJSON.Provider>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/suspense-service@0.2.6/dst/umd/suspense-service.js"></script>
<div id="root"></div>

You can even paginate the results and render a functioning "previous" and "next" button using the response data if you enable concurrent mode:

const { useEffect, useState, unstable_useTransition: useTransition } = React;
const { unstable_createRoot: createRoot } = ReactDOM;
const { createService, createStateContext, useService, useStateContext } = SuspenseService;

const FetchJSON = createService((url) => (
  fetch(url).then((res) => res.json())
));

const Page = createStateContext();

function useFetchJSON() {
  return useService(FetchJSON);
}

function usePageState() {
  return useStateContext(Page);
}

function People() {
  const data = useFetchJSON();
  const [page] = usePageState();

  const list = data.results.map(({ name, url }) =>
    <li key={url}>
      {name}
    </li>
  );

  return (
    <div>
      <p>Page {page}</p>
      <ul>
        {list}
      </ul>
      <p>
        <PageButton delta={-1} disabled={!data.previous}>
          Previous
        </PageButton>
        {' '}
        <PageButton delta={+1} disabled={!data.next}>
          Next
        </PageButton>
      </p>
    </div>
  );
}

function PageButton({ delta, disabled, children }) {
  const [, setPage] = usePageState();
  const [startTransition, isPending] = useTransition();

  const onClick = () => {
    startTransition(() => {
      setPage(prev => prev + delta);
    });
  };

  return (
    <button disabled={disabled || isPending} onClick={onClick}>
      {isPending ? 'Loading...' : children}
    </button>
  );
}

function App() {
  return (
    <Page.Provider value={1}>
      <Page.Consumer>
        {page => (
          <FetchJSON.Provider
            request={`https://swapi.dev/api/people/?page=${page}`}
            fallback={<p>Loading...</p>}
          >
            <People />
          </FetchJSON.Provider>
        )}
      </Page.Consumer>
    </Page.Provider>
  );
}

createRoot(document.getElementById('root')).render(<App />);
<script crossorigin src="https://unpkg.com/react@experimental/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@experimental/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/suspense-service@0.2.6/dst/umd/suspense-service.js"></script>
<div id="root"></div>



回答3:


Use a dependency array of page so that the requestData only gets called on mount, and after a response of the prior page comes back. Don't use while - that'll block, preventing the app from loading at all.

You also need to fix the request you make - .json returns a Promise, you need to wait for it to resolve before accessing the result values.

const { useState, useEffect } = React;
function App() {
    const [page, setPage] = useState(1)
    const [people, setPeople] = useState([]);
    const [gotEveryone, setGotEveryone] = useState(false)
    const [error, setError] = useState(null)

    useEffect(() => {
        if (!gotEveryone && !error) {
            fetch(`https://swapi.dev/api/people/?page=${page}`)
              .then(res => {
                  if (res.status === 404) setGotEveryone(true);
                  else return res.json()
                      .then((result) => {
                          console.log('got result', page);
                          // if people or page could be set elsewhere,
                          // use the callback form instead below
                          setPeople(people.concat(result.results));
                          setPage(page + 1);
                      });
              })
              .catch((err) => {
                  setError(res.err);
              })
        }
    }, [page]);

    return (
        <div>
            <ul>
                {people.map((person, index) =>
                    <li key={index}>
                        {person.name}
                    </li>
                )}
            </ul>
        </div>
    )
}

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>



回答4:


Here's another approach if you're tending to load all people in your component once without pagination or more loading:

  const requestData = async (page, result = []) => {
    const response = await fetch(`https://swapi.dev/api/people/?page=${page}`);
    let data = await response.json();
    if (response.status === 200) {
      result.push(...data.results);
      return requestData(++page, result);
    }

    console.log(result); // all people
    return data;
  };

  useEffect(() => {
    requestData(1, []);
  }, []);

You're calling requestData once in the useEffect then you're recursively calling it with different pages, and saves the result in it without any state mutations every time, And in the final call you setting the state safely.

This's not the complete code for your question, but another approach, You can edit this code, to mutate page, or setting errors.



来源:https://stackoverflow.com/questions/65873133/best-way-to-request-unknown-number-of-api-pages-in-useeffect-hook

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!