I was wondering what the best practice is for storing/using data from an API in a react app. Here's a hypothetical example:
A simple app that fetches blog posts from an API and a control bar above the posts that can filter the posts by date, category, etc.
Which approach would be better:
1). Fetch every single post with useEffect, for example, and then store the returned data in state, and then create the filtering functions which filter the data that is stored in state.
2). Fetch every single post on first render, again with useEffect, and then create filtering functions which make multiple requests to the API for the required data.
Something like this
const BlogPage = () => {
//state where store posts (if u don't use redux)
const [data, setData] = useState([])
//request with or without params
const fetchData = async (queryParams = '') => {
// res = await request....
setData(res)
}
useEffect(() => {
fetchData()
}, [])
return <>
<Filters onSubmit={fetchData}/>
<Table data={data}/>
</>
}
You can update this with loaders, empty data render templates, adding redux or other
If you don't have an API with query params and you have no server pagination you must write filter function and replace <Filters onSubmit={fetchData}/> to <Filters onSubmit={filter}/>. The result of this function must call setData.
Related
currently i am using SWR to data fetching, i trying to use Mutation feature of SWR to refetch the new data but this occurred a problem when i am calling mutate() by key was added new query params.
Here's my code not working:
import useSWR, { useSWRConfig } from 'swr'
function Profile () {
const { mutate } = useSWRConfig()
const { data } = useSWR('/api/post', fetcher)
return (
<div>
<h1>Title post {data.title}.</h1>
<button onClick={() => {
mutate('/api/post?author=1&pricing=1')
}}>
View more information of this post!
</button>
</div>
)
}
I read docs from SWR and i know the key of the mutate should be the same to key in useSWR() but in my case need more query params to get the corresponding data
How can i solve this problem?
Helps me please!
I wouldn't recommend using mutate in this scenario because the key (the URL) you want to use in the mutation is different from the original one. When mutate is called it'll update the cache for '/api/post' which will then contain data from '/api/post?author=1&pricing=1' in it.
As an alternative, I'd suggest you make the key an array in the useSWR call so that multiple arguments can be passed to the fetcher.
const [queryParams, setQueryParams] = useState('')
const { data } = useSWR(['/api/post', queryParams], fetcher)
Then, in your button onClick handler, you can update the queryParams state value to trigger a re-render and initiate a new request with the query parameters.
<button onClick={() => {
setQueryParams('?author=1&pricing=1')
}}>
View more information of this post!
</button>
You also need to modify the fetcher function slightly to expect multiple arguments, and also append the query params you pass to the URL.
const fetcher = (url, queryParams = '') => {
// Example fetch to demonstrate the logic
return fetch(`${url}${queryParams}`)
}
With this change you now have different keys (and cached data) for each URL you make a request for.
I want to achieve the following.
Action from redux is imported in this component. It fetches data from firebase, which I use as a database. Data is displayed inside this component.
My problem is that data is fetched every time component is mounted, which is not an ideal scenario.
I would like to prevent that behavior.
Ideal scenario is that data should be fetched only first time component is rendered and no more afterwards. Of course it should also fetch when data is changed otherwise no. I used useCallback, but certainly there is a mistake in the code below. Token and Id are passed when user is authenticated.
import { fetchFavs } from "../../store/actions";
import { useSelector, useDispatch } from "react-redux";
const token = useSelector((state) => state.auth.token);
const id = useSelector((state) => state.auth.userId);
const fetchedFavs = useSelector((state) => state.saveFavs.fetchedFavs);
const dispatch = useDispatch();
const fetchFavsOptimized = useCallback(() => {
dispatch(fetchFavs(token, id));
console.log("rendered");
}, [token, id, dispatch]);
useEffect(() => {
if (token) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token]);
There are two ways to handle this, one explicit and one implicit.
The explicit way would be to add a new boolean item in your redux store and check against that every time you load your component to see if you should load the data from the db or not.
So your redux store may have a new key/value: dataLoaded: false;
In your component now you need to load this from your redux store and check against it in your use effect:
const dataLoaded = useSelector((state) => state.dataLoaded);
useEffect(() => {
if (token && !dataLoaded) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, dataLoaded]);
Don't forget in your fetchFavsOptimized function to update the dataLoaded to true after the loading of the data from the db is complete.
This has the upside that every time you want to load the db from the db again you simply need to set dataLoaded to false.
The implicit way is to check the data length and if that is 0 then execute the load. Assuming that you store data in your redux store in this form: data: [] and your data is an array (you could do the same thing by initiating data as null and check if data !== null) your component would look like this:
const data = useSelector((state) => state.data);
useEffect(() => {
if (token && data.length === 0) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, data]);
Keep in mind that I wouldn't do it like that because if you don't have any data you may end up with an infinite loop, but it's a good technique to use in other cases.
For fetching only once on component mounting: (mind [])
React.useEffect(() => {
fetchFavsOptimized()
}, []) // <- this empty square brackets allow for one execution during mounting
I'm not sure, but the issue of continous calling of useEffect might be [fetchFavsOptimized, token]. Placing fetchFavsOptimized function in [] might not be the best way.
You want to fetch, when data changes, but what data? You mean token ? Or something else?
React.useEffect() hook alows for fetching when data changes with placing this changable data in [here_data] of useEffect. Example:
const [A, setA] = React.useState();
React.useEffect(() => {
// this block of code will be executed when state 'A' is changed
},[A])
The best practise of using useEffect is to separate the conditions in runs with.
If You need to fetch on some data change AND on component mount, write TWO useEffect fuctions. Separate for data change and component mount.
We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}
I am fairly new to graphQL and Apollo. I hope I can make myself clear:
I am fetching data using the apollo/react-hook useQuery. Afterwards I populate a form with the data so the client can change it. When he is done, the data gets send back to the server using useMutation.
Until now, I use onCompleted to store the fetched data in the component state. That looks like this:
import React, { useState } from 'react';
import { TextField } from '#material-ui/core';
const Index = () => {
const [state, setState] = useState(null)
const {data, loading, error} = useQuery<typeof queryType>(query, {
onCompleted: data => {
// modify data slightly
setState(data)
}
})
return (
<TextField value={state} onChange={() => setState(event.target.value)}/>
)
}
The form than uses the values stored in the component state and the form handlers use setState
to change it.
My question now is, if this is the best practice and if the storing of the fetched data in a local component state neccessary.
Because you don't want to just fetch and render the data -- you want to be able to mutate it when the form values change -- we can't just utilize the data as is. Unless you're using uncontrolled inputs and refs to manage your forms (which you probably shouldn't do), then you're going to need to use some component state.
I think your current approach is mostly fine. The biggest downside is that if the query takes a while (maybe the user has a spotty internet connection), there's a window for them to start filling out the form only to have their input overridden once the query completes. You'd have to explicitly disable the inputs or hide the form until loading completes in order to prevent this scenario.
An alternative approach is to split your component into two:
const Outer = () => {
const {data, loading, error} = useQuery(query)
if (!data) {
return null // or a loading indicator, etc.
}
return <Inner data={data}/>
}
const Inner = ({ data }) => {
const [value, setValue] = useState(data.someField)
<TextField value={value} onChange={() => setValue(event.target.value)}/>
}
By not rendering of the inner component until loading is complete, we ensure that the initial props we pass to it have the data from the query. We can then use those props to initialize our state.
Seems like useQuery only has a state for responseId: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useQuery.ts
But you get a refetch function from the useQuery as well.
const { loading, error, data, refetch } = useQuery(...);
Try calling refetch(), after you used useMutation() to update the data. It shouldn't trigger a rerender. You probably have to set your own state for that reason.
Maybe something like:
const handleUpdate = () =>{
setData(refetch());
}
Alternativly you get the data after using useMutation which is also in the state: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useMutation.ts
const [update, { data }] = useMutation(UPDATE_DATA);
data will always be the newest value so you could also do:
useEffect(()=>{
setData(data);
// OR
// setData(refetch());
}, [data])
I am using react-apollo's <Query> component inside my function component to fetch data from Apollo server graphql microservice. I want to update the state of my component using useEffect hook and Query component.
I tried using Query component within useEffect function, but the request is not going without giving any error
const foo = () => {
const [bar, setBar] = useState([]);
const fetchBars = () => (<Query query={fetchQuery}>{({loading, error, data}) => {
if(!loading) console.log(data);
}})
useEffect(() => {
fetchBars();
}, []);
}
Ideally, it should call graphql api and fetch the data but console is not coming up. I tried using axios library instead of Query component and the request is going through, but I don't want to introduce new stuff to do same thing.