问题
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