What is the best way to fetch data in react? - javascript

I currently use contentful for data management, so it's not the usual axios/fetch method this time.
I'm using useContext to share the data to my components, and have an useEffect that sets the state with the new data. What is the problem you ask? The ugly syntax. When i pass the data in the second rerender i can't access the data immediately, the data[0][0] doesn't exist yet, so it throws an error. This results in this disgusting syntax: <h5>{data ? data[0].sys.contentType.sys.id : ""}</h5> It might be "fine", and it "works". But it looks absolutely atrocious to me.
App.jsx
const App = () => {
const contentfulHook = useState(null);
useEffect((e) => {
client.getEntries().then((res) => contentfulHook[1](res.items));
/*
OR - same result
(async () => {
const data = await client.getEntries().then((res) => res.items);
contentfulHook[1](await data);
})();
*/
//Remove preloader when content is loaded
setTimeout(() => {
const preloader = document.getElementById("preloader");
preloader.style.opacity = 0;
preloader.addEventListener("transitionend", (e) => {
preloader.remove();
});
}, 0);
}, []);
console.log(contentfulHook[0]);
return (
<contentfulContext.Provider value={contentfulHook}>
<BrowserRouter>
<Global />
<Pages />
</BrowserRouter>
</contentfulContext.Provider>
);
};
render(<App />, document.getElementById("root"));

I'd suggest two things on that matter:
Encapsulate that request logic in a custom hook, so you can remove that code from your component.
Use data, loading and error pattern (as used by Apollo and probably other libraries).
The result would be something like:
const App = () => {
const { data, loading, error } = useRequest();
return (
<contentfulContext.Provider value={data}>
<BrowserRouter>
<Global />
<Pages />
</BrowserRouter>
</contentfulContext.Provider>
);
};
const useRequest = () => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
client.getEntries()
.then((res) => setData(res.items))
.catch((e) => setError(e))
.finally(() => {
removePreloader();
setLoading(false);
});
}, []);
return { data, loading, error };
}
const removePreloader = () => {
setTimeout(() => {
const preloader = document.getElementById("preloader");
preloader.style.opacity = 0;
preloader.addEventListener("transitionend", (e) => {
preloader.remove();
});
}, 0);
}
render(<App />, document.getElementById("root"));
Either way, you still need to check for the data before accessing the data attributes when rendering. Even if it is on a top-level condition like:
const App = () => {
const { data, loading, error } = useRequest();
return (
<contentfulContext.Provider value={data}>
<BrowserRouter>
<Global />
<Pages />
{data && <h5>{data[0].sys.contentType}</h5>}
</BrowserRouter>
</contentfulContext.Provider>
);
};

Related

How manage data when its already fetched by Axios

I am using a database with MySQL and getting it using Axios and a useEffect. Then I pass my database data to a component using a prop. Like this:
const Component = () => {
//DB
const urlProxy = "/api/cities";
//Hooks
const [data, setData] = useState([]);
//DB Fetch
const fetchData = async () => {
await axios
.get(urlProxy)
.then((res) => {
setData(res.data);
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
return () => {
fetchData();
};
}, []);
return (
<>
<h1>Cities</h1>
<Cities api={data} />
</>
);
};
Inside of Cities Component I want to make an algorithm to manipulate that data, but I get one empty array (from const [data, setData] = useState([]). After a moment I get the fetched data from database.
const Cities = (api) => {
console.log(data) // First Print: [{api:Array(0)}] then [{api:Array(2)}]
return(
<>
...
</>
)
}
So if it prints at first an empty array I would get an error
I was thinking of using a useTimeout() but i don't know if there is a better solution, in order to use data after it's fetched.
All you would need to do is manipluate the data before you set it into your state
and the best way to wait until that is done is to have a loading state that waits for your data to be pulled and then have your useEffect manipulate it.
Something like this should work for you
const urlProxy = "/api/cities";
const Component = () => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
//DB Fetch
const fetchData = async () => {
await axios
.get(urlProxy)
.then((res) => {
// Manipulate your data here before you set it into state
const manipluatedData = manipulateData(res.data)
setData(manipluatedData);
})
.catch((err) => {
console.log(err);
})
.finally(() =>
setLoading(false);
})
};
useEffect(() => {
return () => {
fetchData();
};
}, []);
if(loading){
return 'loading....'
}
return (
<>
<h1>Cities</h1>
<Cities api={data} />
</>
);
};

How to handle state in useEffect from a prop passed from infinite scroll component

I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);

React Native - How to memoize a component correctly?

This is my parent component:
const A = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const isFetching = useRef(false);
...
const fetchMoreData = async () => {
if(isFetching.current) return;
isFetching.current = true;
setIsLoading(true);
try {
const newData = await ...
setData([...data, ...newData]);
}
catch(err) {
...
}
setIsLoading(false);
isFetching.current = false;
}
...
return <B data={data} isLoading={isLoading} onEndReached={fetchMoreData} />
}
And I am trying to memoize my child component B, to avoid unnecessary re-renders. Currently, I am doing the following:
const B = memo(({ data, isLoading, onEndReached }) => {
...
return (
<FlatList
data={data}
isLoading={isLoading}
onEndReached={onEndReached}
/>
);
},
(prevProps, nextProps) => {
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) &&
prevProps.isLoading === nextProps.isLoading;
});
But, I think, that my memoization can cause problems... as I am not adding prevProps.onEndReached === nextProps.onEndReached
But... all works fine :/ Btw I suppose that there can be a little chance to see unexpected things happening because of not adding it. What do you think? Is it necessary to add methods in the areEqual method?

Display no results component when there are no results

I'm trying to show the no results components whenever the api has finished loading and when no results are returned. The issue I'm having, is I am seeing the no results components displayed first for a few seconds and then the results showing whenever the api returns data. I should never want to see the no results component showing if there are results returned from the api.
import React, { useEffect, useState } from 'react';
import NoResults from './NoResults';
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
const isLoading = () => {
if (isResultsLoading) return <ResultsLoader />;
if (results && results.length > 0)
return (
<UserTableWrapper>
<UserTable
data={results}
/>
</UserTableWrapper>
);
return <NoResults heading="No users available." />;
};
useEffect(() => {
let isMounted = true;
const getData = async () => {
if (isMounted) {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) return { users, loading: false };
return { users: null, loading: true };
}
return { users: null, loading: true };
};
getData().then(({ users, loading }) => {
if (isMounted) {
if (users) setResults(users);
setIsResultsLoading(loading);
}
});
return () => {
isMounted = false;
};
}, []);
return (
<>
<h1>Users</h1>
{isLoading()}
</>
);
};
};
export default Users;
Check for the length of results with results.length since results already exists as an empty array.
When you get your data and parse it simply set both the states for results with the data, and isLoading to false.
Perhaps rename the isLoading function to something more representative of what the function does. I've called mine getJSX.
Here's a working example that uses a mock API. (Note I've had to use this without async and await because snippets haven't caught up with the latest Babel version.) You can change the JSON that's returned by the API by uncommenting/commenting out the JSON statements in the first couple of lines.
const {useState, useEffect} = React;
const json= '[1, 2, 3, 4]';
// const json = '[]';
function mockApi() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 3000);
});
}
function Example() {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
function getJSX() {
if (isResultsLoading) return <div>Loading</div>;
if (results.length) {
return results.map(el => <div>{el}</div>);
}
return <div>No users available.</div>;
};
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => {
setResults(data);
setIsResultsLoading(false);
});
}, []);
return <div>{getJSX()}</div>
};
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You should rely on conditional rendering and simplify your logic a little bit.
import React, { useEffect, useState } from "react";
import NoResults from "./NoResults";
const Users = () => {
// This holds the results - default to null till we get a successful API response
const [results, setResults] = useState(null);
// This should be a boolean stating if the API call is pending
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
const getData = async () => {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) {
// if the result is good, store it
setResults(users);
}
// By the way, the api call is finished now
setIsResultsLoading(false);
};
getData();
}, []); // no deps => this effect will run just once, when the component mounts
if (isResultsLoading) {
// Render nothing while API call is pending
return null;
} else {
if (results) {
// The API has returned a good result, so render it!
return (
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
);
} else {
// No good result, render the fallback component
return <NoResults heading="No users available." />;
}
}
};
export default Users;
You've a lot of extraneous conditionals and code duplication (not as DRY as it could be). Try cutting down on the user checks before you've even updated state, and you likely don't need the mounted check. Conditionally render the UI in the return.
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
fetchUsers() // is just an api call
.then(users => {
setResults(users);
})
.catch(error => {
// handle any errors, etc...
})
.finally(() => {
setIsResultsLoading(false); // <-- clear loading state regardless of success/failure
});
}, []);
return (
<>
<h1>Users</h1>
{isResultsLoading ? (
<ResultsLoader />
) : results.length ? ( // <-- any non-zero length is truthy
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
) : (
<NoResults heading="No users available." />
)}
</>
);
};

React page doesn't load the new content

I'm trying to redirect page declaratively.
Here is my Header.js:
const Header = () => {
const [data, setData] = useState({ objects: [] });
useEffect(async () => {
const result = await axios.get(
"http://example.com/api/v1/categories"
);
setData(result.data);
}, []);
return (
<div>
{courses.map( objects => (
<Link to={`/cats/${objects.id}`}>{objects.title}</Link>
))}
</div>
);
};
export default Header;
Here is my App.js
class App extends Component {
render(){
return (
<Router>
<Redirect exact from="/" to="/index" />
<Route path = "/" component = {App}>
<Header/>
<Route path="/cats/:objectId" component={CoursePage} />
</Route>
</Router>
);
}
}
export default App;
Here is the CoursePage.js:
const CoursesPage = (props) => {
let { objectId } = useParams();
const [data, setData] = useState({ courses: [] });
useEffect(() => {
(async () => {
const result = await axios.get(
"http://example.com/api/v1/cats/"+objectId
).catch(err => {
console.error(err);
});
setData(result.data);
})();
}, []);
return (
<Fragment>
{courses.title}
</Fragment>
);
};
export default CoursesPage;
On Header I click to links. It redirects to course page successfully. But when I click again another course link, it doesn't reload the component and it doesn't load the new data. The URL changes, but page is not.
How can I force the page to reload the component?
You should change the dependency array of useEffect in CoursePage to depend on objectId, whenever object id will change, it will rerun the effect.
const CoursesPage = (props) => {
let { objectId } = useParams();
const [data, setData] = useState({ courses: [] });
useEffect(() => {
(async () => {
setData({ courses: [] }); // add this line
const result = await axios.get(
"http://example.com/api/v1/cats/"+objectId
).catch(err => {
console.error(err);
});
setData(result.data);
})();
}, [objectId]); // Update dependency array here
return (
<Fragment>
{courses.title}
</Fragment>
);
}
export default CoursesPage;
This will reset the previous data before to load new one as soon as object id changes.
If you want to remount component completely on param change in url, then you can add an id to your component root element.
return (
<Fragment key={objectId}>
{courses.title}
</Fragment>
);
More Info here...

Categories

Resources