any idea why this custom hook with SWR causes an infinite loop?
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
}
});
return data;
};
I need to fetch data from API and add missing id from URL param. If I use setData(dataSWR.data), everything is fine. The problem occurs when setData({...dataSWR.data}) is used -> loop.
You need to use useEffect based on the scenario. When dataSWR changed the useEffect call again with new data.
You can add the dataSWR as dependencies argument in useEffect hook.
useEffect(() => { do something... }, [dataSWR])
Example:
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<AxiosResponse<Omit<OrganizationModel, 'id'>>>(`organizations/${organizationId}`, API);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
};
},[dataSWR]);
return data;
};
Usage of hook:
const data = useOrganization()
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
I found the solution - useMemo hook:
export const useOrganization = () => {
const { organizationId } = useParams();
const { data } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
const result = useMemo(() => {
if (data && organizationId) {
return { id: organizationId, ...data.data };
}
}, [data, organizationId]);
console.log('useOrganization');
return result;
};
Related
I am trying to render the number of items resulting from a custom hook. This custom takes some time to fetch the data from a database and returns a count (ie: any integer greater or equal to zero).
My current setup is to call the custom hook and push that value into a useState hook to display the current number of items.
However, this is not working. What is currently occurring is that only the first item from the custom hook is returned, and not the updated item.
// A React Component
// gamePlays holds the amount of items returned from the useLoadSpecficRecords hook.
// However, when data initially loads, the length is `0`, but when loading is finished, the length may increase.
// This increased length is not represented in the gamePlays variable.
const gamePlays = useLoadSpecficRecords(today).games.length
// I want to set the initial value of selected to the number of game plays, but only
// `0` is being returned
const [selected, setSelected] = useState({
count: gamePlays,
})
// This useEffect hook and placed gamePlays as a dependency, but that did not update the value.
useEffect(() => {
}, [gamePlays])
These are the logs to indicate that the length does load, but is not being updated in the gamePlays variable:
0
0
0
0
0
2
// useLoadSpecficRecords Hook
import { useState, useEffect } from 'react'
import { API, Auth } from 'aws-amplify'
import { listRecordGames } from '../graphql/queries'
// Centralizes modal control
const useLoadSpecficRecords = (date) => {
const [loading, setLoading] = useState(true)
const [games, setData] = useState([])
useEffect(() => {
fetchGames(date)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [date])
const fetchGames = async (date) => {
try {
const formatedDate = await date
let records = await API.graphql({
query: listRecordGames,
variables: {
filter: {
owner: { eq: username },
createdAt: { contains: formatedDate },
},
},
})
const allGames = records.data.listRecordGames.items
const filteredGames = allGames.map(({ name, players, winners }) => {
return {
gameName: name,
players: players,
winners: winners,
}
})
setLoading(false)
setData(filteredGames)
} catch (err) {
console.error(err)
}
}
return { games, loading }
}
export default useLoadSpecficRecords
In the useEffect in your custom hook useLoadSpecficRecords, change the dependecy list from date to games. This should retrigger the useEffect and you should see the updated data.
Here is the new implementation:
import { useState, useEffect } from 'react';
import { API, Auth } from 'aws-amplify';
import { listRecordGames } from '../graphql/queries';
// Centralizes modal control
const useLoadSpecficRecords = (date) => {
const [loading, setLoading] = useState(true);
const [games, setData] = useState([]);
useEffect(() => {
fetchGames(date);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [games]); <-- dependency list updated!
const fetchGames = async (date) => {
try {
const formatedDate = date;
let records = await API.graphql({
query: listRecordGames,
variables: {
filter: {
owner: { eq: username },
createdAt: { contains: formatedDate },
},
},
});
const allGames = records.data.listRecordGames.items;
const filteredGames = allGames.map(({ name, players, winners }) => {
return {
gameName: name,
players: players,
winners: winners,
};
});
setLoading(false);
setData(filteredGames);
} catch (err) {
console.error(err);
}
};
return { games, loading };
};
export default useLoadSpecficRecords;
I've also removed an unnecessary await you had before date on line 17.
I have a component using apollo-client with react where I was originally using the onError and onCompleted callbacks to set data once it is received, or render an error message. This is how the component with useLazyQuery hook looked:
export const StationCard = ({ stationData }) => {
const { name, parking, stationNumber } = stationData;
const [showMoreInfo, setShowMoreInfo] = useState(false);
const [fetchedData, setFetchedData] = useState(null);
const [error, setError] = useState(false);
const [getMoreInfo, { loading, data }] = useLazyQuery(STATION_DETAILS, {
onCompleted: data => setFetchedData(data.stationWithStationNumber),
onError: error => {
setError(true);
console.error(error);
},
});
useEffect(() => {
if (showMoreInfo) {
getMoreInfo({ variables: { stationNumber } });
}
}, [showMoreInfo]);
return (
// only for demonstration
<div></div>
)
}
Once I refactored it to use the variables data and error that we get by default from useLazyQuery and useQuery, I was able to remove a lot of un-needed and redundant useStates, which I think would also have reduced many re-renders as the state is not being updated as frequently now:
export const StationCard = ({ stationData }) => {
const { name, parking, stationNumber, picture } = stationData;
const [showMoreInfo, setShowMoreInfo] = useState(false);
const [getMoreInfo, { loading, data, error }] = useLazyQuery(STATION_DETAILS);
useEffect(() => {
if (showMoreInfo) {
getMoreInfo({ variables: { stationNumber } });
}
}, [showMoreInfo]);
return (
// only for demonstration
<div></div>
);
};
So are there any use cases when the callbacks are actually useful and preferable over the variables offered by useQuery/useLazyQuery or are the callbacks redundant and non-performant?
I have a data and I put it in the state and I want to add a new value in addition to the content of the data in the object called watched, but this is a problem, thank you for your help.
import React, { useEffect, useState } from "react";
import ListManager from "./component/manager";
const App = () => {
const [getMovies, setMovie] = useState([]);
const [getLoading, setLoading] = useState(true);
useEffect(() => {
fetch("http://my-json-server.typicode.com/bemaxima/fake-api/movies")
.then((response) => response.json())
.then((response) => {
setMovie(response);
setLoading(false);
});
});
useEffect(() => {
return () => {
setMovise(
getMovies.map((item) => ({
id: item.id,
text: item.name,
rate: item.rate,
watched: false,
}))
);
};
});
if (getLoading) {
return "Please wait...";
}
return <ListManager movies={getMovies} />;
};
export default App;
You don't need the second useEffect, you should use the first useEffect to do all your stuff, also you should pass an empty array to useEffect in order to be executed one time.
import React, { useEffect, useState } from "react";
import ListManager from "./component/manager";
const App = () => {
const [getMovies, setMovie] = useState([]);
const [getLoading, setLoading] = useState(true);
useEffect(() => {
fetch("http://my-json-server.typicode.com/bemaxima/fake-api/movies")
.then((response) => response.json())
.then((response) => {
setMovie(response.map((item) => ({
id: item.id,
text: item.name,
rate: item.rate,
watched: false,
})));
setLoading(false);
});
}, []);
if (getLoading) {
return "Please wait...";
}
return <ListManager movies={getMovies} />;
};
export default App;
It looks like you have a typo calling setMovise instead of setMovies.
I'm not sure about your second useEffect. A return in useEffect is often used
for a cleanup function like this (from ReactJS documentation):
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});
I'd just put it normally in the code outside the hook before the return statements.
Your variable naming doesn't fit the usual convention for
useState.
const [getMovies, setMovie] = useState([]);
should be
const [movies, setMovies] = useState([])
In Apollo Client v3 React implementation, I am using hooks to use subscription. When I receive data from subscription I would like to refetch query but only if query has been previously executed and is in cache. Is there a way to achieve this?
I have started by having a lazy query and then checking the cache manually when subscription data received and then trying to execute lazy query and refetch. It works but it just feels clunky...
export const useMyStuffLazyRefetch = () => {
const [refetchNeeded, setRefetchNeeded] = useState<boolean>(false);
const client = useApolloClient();
const [getMyStuff, { data, refetch }] = useLazyQuery<IStuffData>(GET_MY_STUFF);
useEffect(() => {
if (refetchNeeded) {
setRefetchNeeded(false);
refetch();
}
}, [refetchNeeded]);
const refetchIfNeeded = async () => {
const stuffData = client.cache.readQuery<IStuffData>({ query: GET_MY_STUFF });
if (!stuffData?.myStuff?.length) return;
getMyStuff();
setRefetchNeeded(true);
}
return {
refetchIfNeeded: refetchIfNeeded
};
}
useLazyQuery has a prop called called, this is a boolean indicating if the query function has been called,
so maybe you can try this:
export const useMyStuffLazyRefetch = () => {
const [refetchNeeded, setRefetchNeeded] = useState<boolean>(false);
const client = useApolloClient();
const [getMyStuff, { data, refetch, called }] = useLazyQuery<IStuffData>(GET_MY_STUFF);
useEffect(() => {
if (refetchNeeded) {
setRefetchNeeded(false);
if (called) {
refetch();
}
else {
getMyStuff()
}
}
}, [refetchNeeded, called]);
const refetchIfNeeded = async () => {
const stuffData = client.cache.readQuery<IStuffData>({ query: GET_MY_STUFF });
if (!stuffData?.myStuff?.length) return;
getMyStuff();
setRefetchNeeded(true);
}
return {
refetchIfNeeded: refetchIfNeeded
};
}
In case this can help to somebody. I have created a separate hook so the usage is less of an eyesore.
This is the hook to refetch if data is in cache. If the data is not in the cache, Apollo Client errors instead of returning something like undefined or null
import { useState, useEffect } from "react";
import { OperationVariables, DocumentNode, LazyQueryHookOptions, useApolloClient, useLazyQuery } from "#apollo/client";
export default function useLazyRefetch <TData = any, TVariables = OperationVariables>(query: DocumentNode, options?: LazyQueryHookOptions<TData, TVariables>) {
const [refetchNeeded, setRefetchNeeded] = useState<boolean>(false);
const [loadData, { refetch }] = useLazyQuery(query, options);
const client = useApolloClient();
useEffect(() => {
if (refetchNeeded) {
setRefetchNeeded(false);
refetch();
}
}, [refetchNeeded]);
const refetchIfNeeded = (variables: TVariables) => {
try {
const cachecData = client.cache.readQuery<
TData,
TVariables
>({
query: query,
variables: variables
});
if (!cachecData) return;
loadData({ variables: variables });
setRefetchNeeded(true);
}
catch {}
};
return {
refetchIfNeeded: refetchIfNeeded
};
}
And the hook usage example:
const { refetchIfNeeded } = useLazyRefetch<
IStuffData,
{ dataId?: string }
>(GET_MY_STUFF);
//... And then you can just call it when you need to
refetchIfNeeded({ dataId: "foo" });
typescript is complaining in your
useEffect(() => {
if (refetchNeeded) {
setRefetchNeeded(false);
refetch();
}
}, [refetchNeeded]);
refetch() says - Cannot invoke an object which is possibly 'undefined'.ts(2722)
const refetch: ((variables?: Partial<TVariables> | undefined) => Promise<ApolloQueryResult<TData>>) | undefined
and in [refetchNeeded] dependency -
React Hook useEffect has a missing dependency: 'refetch'. Either include it or remove the dependency array.eslintreact-hooks/exhaustive-deps
const refetchNeeded: boolean
I would like to get global information from Github user and his repos(and get pinned repos will be awesome). I try to make it with async await but It's is correct? I've got 4 times reRender (4 times console log). It is possible to wait all component to reRender when all data is fetched?
function App() {
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(`https://api.github.com/users/${username}`);
const respRepos = await axios(`https://api.github.com/users/${username}/repos`);
setData(respGlobal.data);
setRepos(respRepos.data);
};
fetchData()
}, []);
if (data) {
console.log(data, repos);
}
return (<h1>Hello</h1>)
}
Multiple state updates are batched but but only if it occurs from within event handlers synchronously and not setTimeouts or async-await wrapped methods.
This behavior is similar to classes and since in your case its performing two state update cycles due to two state update calls happening
So Initially you have an initial render and then you have two state updates which is why component renders three times.
Since the two states in your case are related, you can create an object and update them together like this:
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
Working demo
Figured I'd take a stab at it because the above answer is nice, however, I like cleanliness.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const Test = () => {
const [data, setData] = useState([])
useEffect(() => {
(async () => {
const data1 = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
const data2 = await axios.get('https://jsonplaceholder.typicode.com/todos/2')
setData({data1, data2})
})()
}, [])
return JSON.stringify(data)
}
export default Test
Using a self invoking function takes out the extra step of calling the function in useEffect which can sometimes throw Promise errors in IDEs like WebStorm and PHPStorm.
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
he made some mistake here:
setGitData({ data: respGlobal.data, repos: respGlobal.data(respRepos.data //it should be respRepos.data});
For other researchers (Live demo):
import React, { useEffect, useState } from "react";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
function MyComponent(props) {
const [error, setError] = useState("");
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
console.log("mount");
const promise = CPromise.from(function* () {
try {
console.log("fetch");
const [respGlobal, respRepos] = [
yield cpAxios(`https://api.github.com/users/${props.username}`),
yield cpAxios(`https://api.github.com/users/${props.username}/repos`)
];
setData(respGlobal.data);
setRepos(respRepos.data);
} catch (err) {
console.warn(err);
CanceledError.rethrow(err); //passthrough
// handle other errors than CanceledError
setError(err + "");
}
}, []);
return () => {
console.log("unmount");
promise.cancel();
};
}, [props.username]);
return (
<div>
{error ? (
<span>{error}</span>
) : (
<ul>
<li>{JSON.stringify(data)}</li>
<li>{JSON.stringify(repos)}</li>
</ul>
)}
</div>
);
}