use SWR with depending request data - reactjs

I'm trying to use SWR to fetch list of users connected to the logged in user id provided by a custom hook.
I can't put useSWR inside either useCallback or useEffect or if (loggedInAdvisor) { ... }... Can't figure out how to do it.
export const fetchDetailedAdvisorPrognoses = (
body: DetailedAdvisorPrognosesRequest
): Promise<DetailedAdvisorPrognoses[]> | null => {
const accessToken = getFromPersistance(ACCESS_TOKEN)
if (!accessToken) {
return null
}
return fetch('https://x/api/v2/advisors/prognoses', {
method: 'POST',
headers: {
...generateDefaultHeaders(),
'Content-Type': 'application/json',
Authorization: getAuthorizationHeader(accessToken),
},
body: JSON.stringify(body), // body data type must match "Content-Type" header
}).then(res => res.json())
}
function Workload(): ReactElement | null {
const { loggedInAdvisor } = useAuthentication()
// yesterday
const fromDate = moment()
.subtract(1, 'day')
.format('YYYY-MM-DD')
// 14 days ahead
const toDate = moment()
.add(13, 'days')
.format('YYYY-MM-DD')
const { data, error } = useSWR<DetailedAdvisorPrognoses[] | null>('fetchWorkloadData', () =>
'detailed',
fetchDetailedAdvisorPrognoses({
advisorIds: [loggedInAdvisor.id], // <-- I want to pause the query until loggedInAdvisor is defined
fromDate,
toDate,
})
)
// todo: display errors better
if (error) {
return <span>Error: {error.message}</span>
}
if (!data) {
return <LoadingV2 isLoading={!data} />
}
if (data && data.length > 0) {
// advisors prognoses is first element in data array
const [first] = data
const days: WorkloadDay[] = Object.keys(first.daysMap).map(date => ({
date,
value: first.daysMap[date],
}))
return <WorkloadLayout before={first.before} days={days} />
}
return null
}

SWR supports Conditional Fetching, instead of using an if branching, you need to pass null as a key (that's the mental modal of React Hooks too):
const { data, error } = useSWR(
loggedInAdvisor ? 'fetchWorkloadData' : null,
() => {...}
)
Updated 2021/12/10:
You can also fetch some data, that based on the result of another request using SWR, too:
const { data: user } = useSWR('/api/user', fetcher)
const { data: avatar } = useSWR(user ? '/api/avatar?id=' + user.id : null, fetcher)
In this case, if user isn't ready the second request will not start since the key will be null. When the first request ends, the second one will start naturally. This is because a re-render will always happen when user changes from undefined to some data.
You can use this method to fetch as many dependent resources as you want, with the best possible parallelism.

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

Can't convert null or undefined with React Hooks

I have a state that I set with Hooks in react, like this :
useEffect(() => {
if (uid !== null) {
axios.get(`${process.env.REACT_APP_API_URL}api/balance/${uid}`)
.then((res) => {
setUserWalletIncomes(res.data[0].incomes)
})
}
}, [uid])
This state, to be clear, give me this :
Everything works fine I can delete my incomes etc.
Problem is, when I delete the last income (which makes the state empty), I have this error and a page blank :
TypeError: Cannot convert undefined or null to object
Here is my request to delete :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `${process.env.REACT_APP_API_URL}api/balanceOneIncome/${uid}`,
data: data
}).then((res) => {
if {(userWalletIncomes === null) setUserWalletIncomes({})}
setUserWalletIncomes(res.data[0].incomes)
setformIncomes(false)
})
}
I tried to add a condition : if {(userWalletIncomes === null) setUserWalletIncomes({})}, but I still have the same problem.
Can someone help me with this ?
[EDIT]
My state is initialized like this :
const [userWalletIncomes, setUserWalletIncomes] = useState({})
PS : If I reload the page, everything is fine.
Is this the problem? (I am assuming you don't really have the weird {} brace positioning as in your post?)
You have this:
if (userWalletIncomes === null) { setUserWalletIncomes({}) }
setUserWalletIncomes(res.data[0].incomes)
But after you call setUserWalletIncomes({}) you fall through and immediately call it again with the null value in the next line. Maybe you just need to explicitly return after setting it to the empty object? Or, to simplify, you could remove that conditional and do this instead:
setUserWalletIncomes(res.data[0].incomes || {})
Answer, if it helps someone, is this :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `${process.env.REACT_APP_API_URL}api/balanceOneIncome/${uid}`,
data: data
}).then((res) => {
if (res.data[0].incomes) {
setUserWalletIncomes(res.data[0].incomes)
} else {
setUserWalletIncomes({})
}
})
}
Just a condition that tells if response is null, then set the state to an empty object.

Delete data in state React Hooks

Maybe it is a dumb question, because I don't find any response on the net for this, but I'm trying to update my state after a axios.delete.
When I add data, I do this and it works fine :
const handleAddIncome = () => {
let incomeName = document.getElementById('newIncomeName').value
let incomeAmount = document.getElementById('newIncomeAmount').value
let data = {
[incomeName]: parseInt(incomeAmount)
}
axios({
method: "put",
url: `http://localhost:5000/api/balance/${uid}`,
withCredentials: true,
data: { incomes: { ...userWalletIncomes, ...data } }
}).then(() => {
setUserWalletIncomes({ ...userWalletIncomes, ...data })
})
}
I also added a bouton that delete the key/value, and this is where I'm stuck.
Here is what I tried, but no success :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `http://localhost:5000/api/balanceOneIncome/${uid}`,
data: data
}).then(() => {
data[key] = null
setUserWalletIncomes(...userWalletIncomes, data)
})
}
PS : the axios delete works fine, my database is updated normally. Not the state
Thanks for help !
[EDIT]
Here is my UseEffect :
useEffect(() => {
if (uid !== null) {
axios.get(`http://localhost:5000/api/balance/${uid}`)
.then((res) => {
if (res.data[0].incomes && res.data[0].fees) {
setUserWalletIncomes(res.data[0].incomes)
setUserWalletFees(res.data[0].fees)
} else if (res.data[0].incomes && !res.data[0].fees) {
setUserWalletIncomes(res.data[0].incomes)
setUserWalletFees({ 'cliquez-ici': '' })
} else if (!res.data[0].incomes && res.data[0].fees) {
setUserWalletIncomes({ 'cliquez-ici': '' })
setUserWalletFees(res.data[0].fees)
}
})
}
}, [uid])
For those who need, I finally chose to change my controller in my backend and make it send the new object after the delete.
I just take that response, and set the new state.
Thanks
Because you already have setUserWalletIncomes({ ...userWalletIncomes, ...data }), I expect userWalletIncomes is an object like { name1: value1, name2: value2 }, right?
Then you have setUserWalletIncomes(...userWalletIncomes, data), use array spread on an object is a runtime error because it's not iterable:
let a = { foo: 42 };
console.log(...a); // TypeError: Found non-callable ##iterator
I guess you still wanted to write this:
let data = { [key]: "" };
setUserWalletIncomes({ ...userWalletIncomes, ...data });
But this only sets the key field to an empty string. To remove the field, you can combine the answer from How do I remove a property from a JavaScript object?
const temp = {...userWalletIncomes};
delete temp[key];
setUserWalletIncomes(temp);

(Refactor/Improve) Loop to make API calls and manupilate Array following the "no-loop-func"

Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};

useEffect and redux a cache trial not working

I have a rather complex setup and am new to React with Hooks and Redux.
My setup:
I have a component, which when first mounted, should fetch data. Later this data should be updated at a given interval but not too often.
I added useRef to avoid a cascade of calls when one store changes. Without useEffect is called for every possible change of the stores linked in its array.
The data is a list and rather complex to fetch, as I first have to map a name to an ID and
then fetch its value.
To avoid doing this over and over again for a given "cache time" I tried to implement a cache using redux.
The whole thing is wrapped inside a useEffect function.
I use different "stores" and "reducer" for different pieces.
Problem:
The cache is written but during the useEffect cycle, changes are not readable. Even processing the same ISIN once again the cache returns no HIT as it is empty.
Really complex implementation. It dawns on me, something is really messed up in my setup.
Research so far:
I know there are libs for caching redux, I do want to understand the system before using one.
Thunk and Saga seem to be something but - same as above plus - I do not get the concept and would love to have fewer dependencies.
Any help would be appreciated!
Component - useEffect
const calculateRef = useRef(true);
useEffect(() => {
if (calculateRef.current) {
calculateRef.current = false;
const fetchData = async (
dispatch: AppDispatch,
stocks: IStockArray,
transactions: ITransactionArray,
cache: ICache
): Promise<IDashboard> => {
const dashboardStock = aggregate(stocks, transactions);
// Fetch notation
const stockNotation = await Promise.all(
dashboardStock.stocks.map(async (stock) => {
const notationId = await getXETRANotation(
stock.isin,
cache,
dispatch
);
return {
isin: stock.isin,
notationId,
};
})
);
// Fetch quote
const stockQuote = await Promise.all(
stockNotation.map(async (stock) => {
const price = await getQuote(stock.notationId, cache, dispatch);
return {
isin: stock.isin,
notationId: stock.notationId,
price,
};
})
);
for (const s of dashboardStock.stocks) {
for (const q of stockQuote) {
if (s.isin === q.isin) {
s.notationId = q.notationId;
s.price = q.price;
// Calculate current price for stock
if (s.quantity !== undefined && s.price !== undefined) {
dashboardStock.totalCurrent += s.quantity * s.price;
}
}
}
}
dispatch({
type: DASHBOARD_PUT,
payload: dashboardStock,
});
return dashboardStock;
};
fetchData(dispatch, stocks, transactions, cache);
}
}, [dispatch, stocks, transactions, cache]);
Action - async fetch action with cache:
export const getXETRANotation = async (
isin: string,
cache: ICache,
dispatch: AppDispatch
): Promise<number> => {
Logger.debug(`getXETRANotation: ${isin}`);
if (CACHE_ENABLE) {
const cacheTimeExceeding = new Date().getTime() + CACHE_NOTATION;
if (
isin in cache.notation &&
cache.notation[isin].created < cacheTimeExceeding
) {
Logger.debug(
`getXETRANotation - CACHE HIT: ${isin} (${cache.notation[isin].created} < ${cacheTimeExceeding} current)`
);
return cache.notation[isin].notationId;
}
}
// FETCH FANCY AXIOS RESULT
const axiosRESULT = ...
if (CACHE_ENABLE) {
Logger.debug(`getXETRANotation - CACHE STORE: ${isin}: ${axiosRESULT}`);
dispatch({
type: CACHE_NOTATION_PUT,
payload: {
isin,
notationId: axiosRESULT,
created: new Date().getTime(),
},
});
}
//FANCY AXIOS RESULT
return axiosRESULT;
}

Resources