how to stop multiple re-renders from doing multiple api calls useEffect? - javascript

I'm new to react functional components and I'm trying to get the weather data on multiple cities on page load but useEffect is now re-rending each call. How can I write this so useEffect doesn't cause re-renders?
function App() {
const [data, setData] = useState([]);
const [activeWeather, setActiveWeather] = useState([]);
useEffect(() => {
const key = process.env.REACT_APP_API_KEY;
const fetchData = async (city) => {
const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
setData((data) => [
...data,
{ description: res.data.weather[0].description, icon: res.data.weather[0].icon, temp: res.data.main.temp, city: res.data.name, country: res.data.sys.country, id: res.data.id },
]);
};
const fetchCities = () => {
const cities = [fetchData("Ottawa"), fetchData("Toronto"), fetchData("Vancouver"), fetchData("California"), fetchData("London")];
Promise.all(cities).catch((err) => {
console.log(err);
});
};
fetchCities();
}, []);

You can make the fetchData function to return the data you need without updating the state, then you can fetch x amount of cities and only when all of the requests complete update the state.
Note that if one of the requests inside Promise.all fail, it will go to the catch block without returning any data back, basically all or nothing
const key = process.env.REACT_APP_API_KEY
const fetchCity = async city => {
const { data } = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`,
)
return {
description: data.weather[0].description,
icon: data.weather[0].icon,
temp: data.main.temp,
city: data.name,
country: data.sys.country,
id: data.id,
}
}
function App() {
const [cities, setCities] = useState([])
const [activeWeather, setActiveWeather] = useState([])
useEffect(() => {
const fetchCities = async () => {
const citiesData = await Promise.all(
['Ottawa', 'Toronto', 'Vancouver'].map(fetchCity),
)
setCities(prevState => prevState.concat(citiesData))
}
fetchCities()
}, [])
}

You can use Promise.all and then call setData once. something like this:
useEffect(() => {
const fetchCity = (city) => axios.get(`${base}/${city}`);
const cities = ["Ottawa", "Toronto"];
const promises = cities.map(fetchCity);
Promise.all(promises).then((responses) => {
setData(cities.map((city, index) => ({ city, ...responses[index] })));
});
}, []);

Related

I want to be able to delete an object from the api and re render the function without having to manually refresh the page, how can I do that?

const Notes = () => {
const history = useNavigate();
const [apiData, setApiData] = useState([]);
useEffect(() => {
axios
.get(`https://6390acc765ff4183111b53e9.mockapi.io/notes`)
.then((getData) => {
setApiData(getData.data);
});
}, []);
const onDelete = (id) => {
axios
.delete(`https://6390acc765ff4183111b53e9.mockapi.io/notes/${id}`)
.then(() => {
history("/notes");
});
};
This way I can delete the note that i fetched earlier, but it still appears on the screen until I refresh manually. It doesn't also go to /notes because i am already on /notes
You can either return the updated data in the delete response to update the local state, or you can trigger a refetch of the data after a successful deletion.
Refetch Example:
const Notes = () => {
const history = useNavigate();
const [apiData, setApiData] = useState([]);
const fetchNotes = useCallback(async () => {
const getData = await axios
.get(`https://6390acc765ff4183111b53e9.mockapi.io/notes`);
setApiData(getData.data);
}, []);
useEffect(() => {
fetchNotes();
}, [fetchNotes]);
const onDelete = async (id) => {
await axios
.delete(`https://6390acc765ff4183111b53e9.mockapi.io/notes/${id}`);
fetchNotes();
history("/notes");
};
...
Returned response Example*:
const Notes = () => {
const history = useNavigate();
const [apiData, setApiData] = useState([]);
useEffect(() => {
axios
.get(`https://6390acc765ff4183111b53e9.mockapi.io/notes`)
.then((getData) => {
setApiData(getData.data);
});
}, []);
const onDelete = async (id) => {
const getData = await axios
.delete(`https://6390acc765ff4183111b53e9.mockapi.io/notes/${id}`);
setApiData(getData.data);
history("/notes");
};
...
*Note: This requires updating the backend code to return the updated data in the response.

Store copy of data state before applying and after clearing filter

I have a simple app that calls an API, returns the data (as an array of objects), sets a data state, and populates a few charts and graphs.
const loadData = async () => {
const url = 'https://my-api/api/my-api';
const response = await fetch(url);
const result = await response.json();
setData(result.data);
}
After setting the data, the data state is sent to every component and everything is populated. I created a filters pane that can filter the existing, populated data (for example, a gender filter that filters the data on the selected gender). What I did, and it's obviously wrong, is created an onChange handler that filters the data to the selected gender then uses the setData (sent as a prop; also the state variable, data) to set the filtered data. When I clear the filter, the original, non-filtered data is replaced by the filtered data so the original data is lost.
const genderFilterHanlder = (e) => {
const filteredData = data.filter(x => x.gender === e.target.value);
setData(filteredData);
}
I tried creating an intermediary state the preserves the original data then upon clearing the filters, it sets the data (setData) to the original. But this breaks when I have a filter that allows you to choose multiple values (like multiple languages; I can choose one language, clear it successfully, but if I choose two languages, then clear one, it breaks as the data is now the first chosen filter data).
How would I go about this?
I'd leave data itself alone and have a separate filteredData state member that you set using an effect:
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
// ...
useEffect(() => {
const filteredData = filter ? data.filter(/*...apply filter...*/) : data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
// ...
// ...render `filteredData`, not `data`...
Then your change handler just updates filter (setFilter(/*...the filter...*/)).
That way, any time the filter changes, or any time data changes, the data gets filtered and rendered.
Live Example:
const { useState, useEffect } = React;
const Child = ({data}) => {
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
useEffect(() => {
if (!filter) {
setFilteredData(data);
return;
}
const lc = filter.toLocaleLowerCase();
const filteredData = filter
? data.filter(element => element.toLocaleLowerCase().includes(lc))
: data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
return <div>
<input type="text" value={filter} onChange={({currentTarget: {value}}) => setFilter(value)} />
<ul>
{filteredData.map(element => <li key={element}>{element}</li>)}
</ul>
</div>;
};
const greek = [
"alpha",
"beta",
"gamma",
"delta",
"epsilon",
"zeta",
"eta",
"theta",
"iota",
"kappa",
"lambda",
"mu",
"nu",
"xi",
"omicron",
"pi",
"rho",
"sigma",
"tau",
"upsilon",
"phi",
"chi",
"psi",
"omega",
];
const initialData = greek.slice(0, 4);
const Example = () => {
const [data, setData] = useState(initialData);
useEffect(() => {
const handle = setInterval(() => {
setData(data => {
if (data.length < greek.length) {
return [...data, greek[data.length]];
}
clearInterval(handle);
return data;
});
}, 800);
return () => {
clearInterval(handle);
};
}, []);
return <Child data={data} />;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
You need to have two states in your case. You can follow below example.
const data = [
{ id: 1, name: "Mahela" },
{ id: 2, name: "Sangakkara" },
{ id: 3, name: "Dilshan" },
{ id: 4, name: "Malinga" },
{ id: 5, name: "Rangana" },
{ id: 6, name: "Kulasekara" }
];
const [inputValue, setInputValue] = useState("");
const [filtered, setFiltered] = useState([]);
const inputChangeHandler = (e) => {
setInputValue(e.target.value);
setFiltered(data.filter((item) => item.name === e.target.value));
};
return (
<div className="App">
<input type="text" value={inputValue} onChange={inputChangeHandler} />
{filtered.length > 0 && filtered.map((item) => <p>{item.name}</p>)}
{filtered.length === 0 && data.map((item) => <p>{item.name}</p>)}
</div>
);
Here I have used constant for data array. You can use your data state. Don't replace data state with new data instead use second state for the filtered data.
dont change your state after filtering .
hold your original data and get a copy of it in a variable and do your stuff on it like filtering. then use that variable for rendering the filtered data and if you want to clear the input or anything you can use your original state that you have been storing.
const FilterData = () => {
const [data, setData] = useState([]);
const [select, setSelect] = useState();
const loadData = async () => {
const url = "https://my-api/api/my-api";
const response = await fetch(url);
const result = await response.json();
setData(result.data);
};
const filteredData = data.filter((item) => {
if (!select) return null;
return item.gender !== select;
});
return (
<div>
<select>...</select>
{filteredData.map(...)}
</div>
);
};
In this value must the male/female (or any) and filter must be the type (key) now on change on the filter or value filterData gives you the result
use filterData to render elements and data state will remain constant always.
const[data, setDate] = useState([]);
const[filterData, setFilterData] = useState([]);
const[filter, setFilter] = useState('gender');
const[value, setValue] = useState('male');
const fetchData = () => {
const url = '';
const data = await fetch(url);
setData([...data]);
setFilterData([...data]);
}
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
//initially I have set the filter to gender, but it should be null, once the user selects any filter change the state of the filter, so for the first time when the screen mounts else part executes.
if(filter){
const newFilterData = data.filter(item => item[filter] == value);
setFilterData([...newFilterData]);
}
}, [filter, value]);

How can I run multiple queries on load of functional component using UseEffect and get result in render method?

I have the following functional component where, on load of the component, it needs to loop through an array and run some async queries to populdate a new array I want to display in render method.
import React, { useEffect, useState, useContext } from 'react';
import { AccountContext } from '../../../../providers/AccountProvider';
import { GetRelationTableCount } from '../../../../api/GetData';
import { getTableAPI } from '../../../../api/tables';
const RelatedRecordsPanel = (props) => {
const { userTokenResult } = useContext(AccountContext);
const { dataItem } = props;
const [relatedTableItems, setRelatedTableItems] = useState([]);
useEffect(() => {
const tempArray = [];
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
tempArray.push(TableIconInfo);
})
);
};
fetchData();
setRelatedTableItems(tempArray)
}, []);
return (
<div>
{relatedTableItems.length > 0 ? <div>{relatedTableItems.name}</div> : null}
</div>
);
};
In the above code, the queries run correctly and if I do a console.log in the loop, I can see if fetches the data fine, however, the array is always [] and no data renders. How do I write this async code such that it completes the queries to populate the array, so that I can render properly?
Thx!
You aren't using the return value of the Promise.all and since all your APIs are async, the tempArray is not populated by the time you want to set it into state
You can update it like below by waiting on the Promise.all result and then using the response
useEffect(() => {
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
return TableIconInfo;
})
);
};
fetchData().then((res) => {
setRelatedTableItems(res);
});
}, []);

How to set value of react hook in promise?

I am getting a data from the promise and I want to set it in react hook, it does but it makes infinite requests and re-rendering of the page, also I want some specific only data to fill
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
setRows(newData);
});
});
}
fetchData();
});
You should add dependencies to your useEffect hook. It is the second argument of this hook.
useEffect(() => {
// your code
}, [deps]);
deps explanation:
no value: will execute effect every time your component renders.
[]: will execute effect only the first time the component renders.
[value1, value2, ...]: will execute effect if any value changes.
For further reading, I highly recommend this blog post.
Move setRows call out of the forEach loop and include user.id into the dependency array
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
});
setRows(newData);
});
}
fetchData();
}, [user.id]);

Edit data when using a custom data fetching React Hook?

I've been trying to make a chart with data fetched from an API that returns data as follows:
{
"totalAmount": 230,
"reportDate": "2020-03-05"
},
{
"totalAmount": 310,
"reportDate": "2020-03-06"
}
...
The date string is too long when displayed as a chart, so I want to shorten it by removing the year part.
2020-03-06 becomes 03/06
Following a great tutorial by Robin Wieruch, I now have a custom Hook to fetch data:
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
setData(data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }];
};
Along with my charting component written in React Hooks:
const MyChart = () => {
const [{ data, isLoading, isError }] = useDataApi(
"https://some/api/domain",
[]
);
useEffect(() => {
// I'm using useEffect to replace every date strings before rendering
if (data) {
data.forEach(
d =>
(d.reportDate = d.reportDate
.replace(/-/g, "/")
.replace(/^\d{4}\//g, ""))
);
}
}, [data]);
return (
<>
<h1>My Chart</h1>
{isError && <div>Something went wrong</div>}
{isLoading ? (
. . .
) : (
<>
. . .
<div className="line-chart">
<MyLineChart data={data} x="reportDate" y="totalAmount" />
</div>
</>
)}
</>
);
};
The above works. But I have a feeling that this might not be the best practice because useEffect would be called twice during rendering. And when I try to adopt useReducer in my custom Hook, the code does not work anymore.
So I'm wondering what is the best way to edit data in this circumstance?
You could create a mapping function for your data, which is then used by the hook. This can be done outside of your hook function.
const mapChartDataItem = (dataItem) => ({
...dataItem,
reportDate: dataItem.reportDate.replace(/-/g, "/").replace(/^\d{4}\//g, ""))
});
The reportDate mapping is the same code as you have used in your useEffect.
Then in your hook function:
const data = await response.json();
// this is the new code
const mappedData = data.map(mapChartDataItem);
// change setData to use the new mapped data
setData(mappedData);
Doing it here means that you're only mapping your objects once (when they are fetched) rather than every time the value of data changes.
Update - with injecting the function to the hook
Your hook will now look like this:
const useDataApi = (initialUrl, initialData, transformFn) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
// if transformFn isn't provided, then just set the data as-is
setData((transformFn && transformFn(data)) || data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url, transformFn]);
return [{ data, isLoading, isError }];
};
Then to call it, you can use the following:
const mapChartDataItem = (dataItem) => ({
...dataItem,
reportDate: dataItem.reportDate.replace(/-/g, "/").replace(/^\d{4}\//g, ""))
});
const transformFn = useCallback(data => data.map(mapChartDataItem), []);
const [{ data, isLoading, isError }] = useDataApi(
"https://some/api/domain",
[],
transformFn
);
For simplicity, because the transformFn argument is the last parameter to the function, then you can choose to call your hook without it, and it will just return the data as it was returned from the fetch call.
const [{ data, isLoading, isError }] = useDataApi(
"https://some/api/domain",
[]
);
would work in the same was as it previously did.
Also, if you don't want (transformFn && transformFn(data)) || data in your code, you could give the transformFn a default value in your hook, more along the lines of:
const useDataApi = (
initialUrl,
initialData,
transformFn = data => data) => {
// then the rest of the hook in here
// and call setData with the transformFn
setData(transformFn(data));
}

Categories

Resources