Correct way to update react component from secondary source - reactjs

I'm kind of new with React Hooks and I've encountered a problem when making a component. My App has a simple form with a few fields and a "Calculate" button which fetches info from an API and displays the results on a table. The app uses two currencies, they can be switched with a pair of buttons. What I want is to update the table(re fetch the data) when currency is changed, but only is there was already something calculated via the main "Calculate" button before changing the currency. My component is something along the lines of:
const ProductionCosts = () => {
const [data, setData] = useState({});
const [useXCurrency, setUseXCurrency] = useState(true);
const calcCosts = useCallback(async () => {
fetchCalcData(args);
}, [args]);
useEffect(() => {
if (Object.keys(data).length > 0) //check data isn't empty, hence it was already calculated
fetchCalcData();
}, [useXCurrency]);
return (
......
);
};
Doing something similar to the above works, but the linter will say that data needs to be in the dependency list of the useEffect, but adding it will result on a loop given that fetchCalcData modifies data and triggers the effect, I DO know that the linter suggestions aren't absolute, but at the same time I know that there must be a better way. So besides adding Boolean flags or something like that, there is a better approach to this case?

Typically you want to use a refenence with the initial value and update it on success, on next useEffect the condition will be falsy:
const ProductionCosts = () => {
const [data, setData] = useState({});
const dataRef = useRef(data);
useEffect(() => {
if (Object.keys(dataRef.current).length > 0) {
const data = // fetch your data
dataRef.current = data;
}
}, [useXCurrency]);
return <></>;
};

Related

How to create a copy of an array from an API to sort it?

I want to create a copy of an array from an api to avoid using "data?.map", in order to be able to sort it later in a table.
my use fetch hook:
import { useEffect, useState } from "react";
import axios from "axios";
import { BASE_URL } from "../constants/Constants";
const useFetchData = (url: string) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
const options = {
method: "GET",
url: `${BASE_URL}${url}`,
};
axios
.request(options)
.then((response) => {
setData(response.data);
})
.catch((error) => {
setError(error);
console.error(error);
})
.finally(() => {
setLoading(false);
});
}, [url]);
return { data, loading, error };
};
export default useFetchData;
my Table component:
const { data, loading, error } = useFetchData(SPOT_URL);
const [searchTerm, setSearchTerm] = useState("");
// Table data that displays all the entries or the ones that have the value of the name or
// country inside the SearchInput component
const tableData = data?.filter(
(val: { country: string; name: string }) =>
(searchTerm === "" && val) ||
(val.country.toLowerCase().includes(searchTerm.toLowerCase()) && val) ||
(val.name.toLowerCase().includes(searchTerm.toLowerCase()) && val)
);
...
// After the tableData function is complete, the table rows are generated
const dashboardTableContent = tableData?.map(
(item:) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.country}</td>
<td>{item.lat}</td>
<td>{item.long}</td>
<td>{item.probability}%</td>
<td>{item.month}</td>
</tr>
)
);
...
return (<tbody>{dashboardTableContent}</tbody>)
Is there a way to avoid using the question mark and start using just "data" ?
What I am trying to do is a sortable table ascending descending like in this link https://www.smashingmagazine.com/2020/03/sortable-tables-react/
In this link is a spread operator, which I can't use in my code because I don't have the array in the beginning and it renders a white page.
If all you want is to avoid the question mark, you can just do
const [data, setData] = useState([]);
Instead of
const [data, setData] = useState(null);
You will get an empty array from the start
Maybe I am wrong, but I will try to help you based on what I have understood reading your question.
If you want to make a copy of an array, you can create a new one, and push everything into it, like this:
const newArray = [];
this.newArray.push(...oldArray);
Now, you can manipulate however you want your new array.
EDIT: I answered before you edited your question, so you can't use my method. Or maybe you can make it asynchronous.
you can try declaring data as an empty array in its initial state.
const [data, setData] = useState([]);
this is one way you can avoid the data?.map later since its initial value is already an empty array and javascript won't complain if the data is null
The optional chaining operator [?.][1] is a guard against a potentially undefined object whose property you want to access. In your case, data will be undefined until axios' fetch operation completes and so data.filter would throw an error.
To solve this you need to decide how you want the component to behave before data is defined, while the page is still loading. Some options:
Pretend the table is empty and let more data come in later and update the table. You can do this by const tableData = data ?? [] and then build your table on the tableData.
Don't render the table until data is loaded. If it is not, render something like loading instead.
Regardless of approach, you have to handle the case that data is undefined and show your users something reasonable until the data is available.

unable to set localstorage with product as the state initially changes

i am working on eCommerce project , i am currently facing a problem where as user clicks on add to cart button the code setCart([...cart,item]) runs since the initial state of cart is empty array like const [cart,setCart] = useState([]) therefore as i try to save it on localstorage it gives me empty array and then start saving data in second array
code:
const [cartValue,setCartValue] = useState(0)
const [cart,setCart] = useState([])
const cartValueIncreaserandSetter = (item) =>{
setCart([...cart,item])
setCartValue(cartValue+1)
localStorage.setItem("items",JSON.stringify(cart))
}
i want the localstorage to immediately save the first item added but the localstorage gives [] array on first click on add to cart
When you have side effect in your code (which is the case when you manipulate the localstorage) you should use the useEffect hook.
I suggest you to rewrite your code like this :
const [cartValue,setCartValue] = useState(0)
const [cart,setCart] = useState([])
const cartValueIncreaserandSetter = (item) => {
setCart([...cart, item])
setCartValue(cartValue+1)
}
useEffect(() => {
localStorage.setItem("items",JSON.stringify(cart))
}, [cart]);
That happens because the useState hook is asynchronous.
try using the existing cart and append the item (which came as a parameter) like this:
localStorage.setItem("items",JSON.stringify([...cart,item]))
useEffect(() => {
if (cart.length > 0) localStorage.setItem("items", JSON.stringify(cart));
}, [cart]);

React hooks with AJAX requests

I am using React and I was wondering if I am doing things correctly.
I want to make multiple ajax requests in React for multiple datalists for inputboxes.
I do something like this:
This is my fetch function
function GetData(url, processFunc) {
...
jquery ajax request using url
...
if (status === 'success') {
if (processFunc) {
processFunc(data);
}
}
}
A solution which only displays the results that were the fastest.
function ComponentA() {
const [ajaxDataA, setAjaxDataA] = useState(null);
const [ajaxDataB, setAjaxDataB] = useState(null);
const [ajaxDataC, setAjaxDataC] = useState(null);
const [dataA, setDataA] = useState(null);
...dataB..
...dataC..
const exampleFunctionA = function(data) {
..processes data into result
setDataA(result);
}
const exampleFunctionB = ....
const exampleFunctionC = ...
useEffect( () => {
GetData(url_1, exampleFunctionA);
GetData(url_2, exampleFunctionB);
GetData(url_3, exampleFunctionC);
}, []);
return (<>
...
{dataA}
{dataB}
...
<>);
}
B Solution is why Im asking this question. This works fine but I'm not sure it is correct or this is how hooks were meant to use.
function ComponentA() {
const [ajaxDataA, setAjaxDataA] = useState(null);
const [ajaxDataB, setAjaxDataB] = useState(null);
const [ajaxDataC, setAjaxDataC] = useState(null);
const [dataA, setDataA] = useState(null);
...dataB..
...dataC..
useEffect( () => {
GetData(url_1, setAjaxDataA);
GetData(url_2, setAjaxDataB);
GetData(url_3, setAjaxDataC);
}, []);
useEffect( () => {
..processes data into result
setDataA(result);
}, [ajaxDataA]);
..useEffect ... ,[ajaxDataB] ...
... [ajaxDataC] ...
return (<>
...
{dataA}
{dataB}
...
<>);
}
I have found this solution so I dont repeat the logic. I dont use this solution because I have to process the data so basically instead of
const a = GetData(..., processFunc);
I'd use this solution. And so I would still need the useEffects that watch when it refreshes
const a = useFetch(...)
Ajax request won't display in react render function
so basically the question is:
Is solution B a good solution?
Okay well.. I leave the question up if anyone else had gone on this awful path. So basically I was waiting for the AJAX requests to finish loading with the useEffect hook
At first it looked pretty logical:
I send the request.
when it's done it calls the useState hooks' setter function
the state variable updates
useEffect hook triggers
and I do something with the arrived data in the useEffect body
This was major dumb dumb
since it refreshed the state like it was forced to, having a gun pointed at its head
It caused inconsistencies: A child component would want to use the AJAX data before it arrived. It was like a random number generator. Half the time the page loaded and the other half it died running into nullreference exceptions.
solution: Promise

useEffect re-renders too many times

I have this component, that needs to fetch data, set it to state and then pass it to the children.
Some of the data also needs to be set in context.
My problem is that using useEffect, once called the API, it will re-render for each setvalue() function I need to execute.
I have tried passing to useEffect an empty [] array, still getting the same number of re-renders, due to the fact that the state is changing.
At the moment the array is containg the set...functions to prevent eslint to throw warnings.
Is there a better way to avoid this many re-renders ?
const Home = (props) => {
console.log("TCL: Home -> props", props);
const classes = useStyles();
const [value, setValue] = React.useState(0);
//CONTEXT
const { listSavedJobs, setListSavedJobs, setIsFullView} = useContext(HomeContext);
const {
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
} = useContext(UserContext);
// STATE
const [searchSettings, setSearchSettings] = useState([]);
const [oppData, setOppData] = useState([]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = index => {
setValue(index);
};
//API CALLS
useEffect(() => {
const triggerAPI = async () => {
setIsFullView(false);
const oppResponse = await API.getOpportunity();
if(oppResponse){
setOppData(oppResponse.response);
}
const profileResponse = await API.getUserProfile();
if(profileResponse){
setUserName(profileResponse.response.first_name);
setUserLastName(profileResponse.response.last_name);
setUserEmail(profileResponse.response.emailId);
}
const profileExtData = await API.getUserProfileExt();
if(profileExtData){
setAvatarProfile(profileExtData.response.avatar);
setListSavedJobs(profileExtData.response.savedJobs);
setSearchSettings(profileExtData.response.preferredIndustry);
}
};
triggerAPI();
}, [
setOppData,
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
setListSavedJobs,
setIsFullView,
]);
...```
Pass just an empty array to second parameter of useEffect.
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
Source
Edit: Try this to avoid rerenders. Use with caution
Only Run on Mount and Unmount
You can pass the special value of empty array [] as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect like this:
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial');
useEffect(() => {
// This effect uses the `value` variable,
// so it "depends on" `value`.
console.log(value);
}, [value])
For more clarification useEffect
If you are using React 18, this won't be a problem anymore as the new auto batching feature: https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
If you are using an old version, can refer to this solution: https://statics.teams.cdn.office.net/evergreen-assets/safelinks/1/atp-safelinks.html

react hooks (useEffect) infinite loop clarification

This question is related to this previous question of mine. I have written two custom hooks. The first is useFetchAccounts whose job is to fetch some data about user accounts and looks like this:
// a fake fetch for the sake of the example
function my_fetch(f, delay) {
setTimeout(f, delay)
}
function useFetchAccounts() {
const [data, setData] = useState({accounts: [], loaded: false})
useEffect(() => {
my_fetch(() => {setData({accounts: [{id:1},{id:2}], loaded: true})}, 3000)
}, [])
return data
}
Nothing special about this hook, simply fetches the ids of the accounts.
Now I have another hook, useFetchBalancesWithIDs(account_ids, accounts_loaded), which is meant to take the ids from the previous step and fetch the balances of these accounts if the accounts have been loaded. It looks like this:
function useFetchBalanceWithIDs(account_ids, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(account_ids.map(id => 42+id))
}, 3000)
}, [account_ids, accounts_loaded])
return balances
}
As you can see if the accounts_loaded is false, it will not perform the fetch. Together I am using them as follows:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const account_ids = accounts.map(account => account.id) // extract ids
const balance = useFetchBalanceWithIDs(account_ids, account_data.loaded)
console.log(balance)
return null
}
Unfortunately, this results in an infinite loop. What does work is changing the useFetchBalancesWithIDs to this:
function useFetchBalanceWithAccounts(accounts, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(accounts.map(account => 42+account.id))
}, 3000)
}, [accounts, accounts_loaded])
return balances
}
which performs the ids extraction within. I'm using it like this:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const balance = useFetchBalanceWithAccounts(accounts, account_data.loaded)
console.log(balance)
return null
}
which runs just fine. So the problem seems to be related to the extraction of the IDs from within the App component, which seems to be triggering the useFetchBalanceWithIDs all the time, even though the account_ids do not change value. Can someone help explain this behaviour please? It's OK for me to use useFetchBalanceWithAccounts but I would like to understand why useFetchBalanceWithIDs doens't work. Thank you!
The issue is that you're using the map function to get an array of your account ids. React uses Object.is to determine if your values in the dependency array has changed in each render cycle. The map function returns a new array, so even though the values in your array are the same, the comparison check evaluates to false, and the effect is run.
Referential equality is used by useEffect, that means that [1,2,3] would not equal [1,2,3]
const data = [{id:1}];
data.map(d=>d.id)===data.map(d=>d.id);//is false
One way to prevent the array from id's from changing reference is combining useState with useEffect in App:
function App() {
//store account id's in app state
const [accountIds, setAccountIds] = useState([]);
const account_data = useFetchAccounts();
const accounts = account_data.accounts;
//only if accounts change re set the id's
useEffect(() => {
setAccountIds(accounts.map(a => a.id));
}, [accounts]);
const balance = useFetchBalanceWithAccounts(
accountIds,
account_data.loaded
);
console.log(balance);
return null;
}

Resources