Edit data when using a custom data fetching React Hook? - javascript

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));
}

Related

How manage data when its already fetched by Axios

I am using a database with MySQL and getting it using Axios and a useEffect. Then I pass my database data to a component using a prop. Like this:
const Component = () => {
//DB
const urlProxy = "/api/cities";
//Hooks
const [data, setData] = useState([]);
//DB Fetch
const fetchData = async () => {
await axios
.get(urlProxy)
.then((res) => {
setData(res.data);
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
return () => {
fetchData();
};
}, []);
return (
<>
<h1>Cities</h1>
<Cities api={data} />
</>
);
};
Inside of Cities Component I want to make an algorithm to manipulate that data, but I get one empty array (from const [data, setData] = useState([]). After a moment I get the fetched data from database.
const Cities = (api) => {
console.log(data) // First Print: [{api:Array(0)}] then [{api:Array(2)}]
return(
<>
...
</>
)
}
So if it prints at first an empty array I would get an error
I was thinking of using a useTimeout() but i don't know if there is a better solution, in order to use data after it's fetched.
All you would need to do is manipluate the data before you set it into your state
and the best way to wait until that is done is to have a loading state that waits for your data to be pulled and then have your useEffect manipulate it.
Something like this should work for you
const urlProxy = "/api/cities";
const Component = () => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
//DB Fetch
const fetchData = async () => {
await axios
.get(urlProxy)
.then((res) => {
// Manipulate your data here before you set it into state
const manipluatedData = manipulateData(res.data)
setData(manipluatedData);
})
.catch((err) => {
console.log(err);
})
.finally(() =>
setLoading(false);
})
};
useEffect(() => {
return () => {
fetchData();
};
}, []);
if(loading){
return 'loading....'
}
return (
<>
<h1>Cities</h1>
<Cities api={data} />
</>
);
};

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

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]);

React component not re-rendering on URL parameter change when using useEffect hook to fetch data

Here's the issue:
I have a component that is meant to be the same structure for ≈ 25 different items/pages. So, I do what anyone would when trying to use React, and I am passing dynamic URL parameters into my API request (pictured below).
const [{ items, isLoading, isError }] = useDataApi(
`http://localhost:5000/api/teams/topspending/${params.team}`,
[],
params.team);
This is simply using a useEffect component that has been separated for simplicity (pictured below).
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
export default useDataApi;
The task is pretty simple. Upon clicking on a simple navigation link in the navbar to change the URL from http://localhost:5000/api/teams/topspending/Team1 to http://localhost:5000/api/teams/topspending/Team2 I am wishing that the SAME component will re-render after fetching NEW data with the UPDATED URL.
I have tried many things... and I can get the component to update after every URL change, BUT the data fetched is still the OLD data!
(I am using React-Router to route the single component to this link)
Ok, I think there are 2 little issues in your code.
Inside the parent function
This is my main function that is going to use your custom hook. If you see, I don't use interpolation because it is not going to be detected by your custom hook. That is why your initialUrl variable (Your URL) in your custom hook never change.
const App = () => {
const [id, setId] = React.useState(1);
const response = useDataApi(
'https://jsonplaceholder.typicode.com/posts/' + id,
[],
id,
);
return (
<>
<div>My id {id}</div>
<button onClick={() => setId(id + 1)}>Click Me!</button>
</>
);
};
Inside the custom hook
It seems to me that you are misunderstanding the setState function provided by react.
Remember that every time you call the setState function is not synchronous. I mean, if you use setUrl(initialUrl), then in the next line of code your state variable url will not necessarily have the values already updated. To know more about it, you can read this: https://reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous
I would suggest using another variable to call the correct URL and change the variable names of your custom hook. I added some comments to your code //Note:
export const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API", effect, initialUrl);
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
// Note: This not sync function
setUrl(initialUrl);
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
// Note: I changed this url variable of your state, to use your initialUrl variable. (this initialUrl parameter should have your UPDATED URL)
const result = await axios(initialUrl, opts);
setItems(result.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [effect]);
return [{ items, isLoading, isError }];
};
I Hope, this can help you!.
setState is asynchronous, so there's no guarantee as to when it will be affected before the next render. There's multiple ways to rewrite your code to work more predictably, but with the examples you've provided the easiest solution is to remove the url state altogether and just use initalUrl in your call to axios.
This isn't great.
So another option would be to keep your url state and add a second useEffect that watches url.
eg.
const useDataApi = (initialUrl, initialData, effect) => {
console.log("start/top Data API");
const [items, setItems] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
setUrl(initialUrl);
}, [effect]);
useEffect(() => {
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
console.log("data loading");
try {
console.log(url, "this should be the new Url");
const result = await axios(url, opts);
setItems(result.data.data);
} catch (error) {
setIsError(true);
}
console.log("data loaded...");
setIsLoading(false);
};
fetchData();
return () => abortCtrl.abort();
}, [url])
return [{ items, isLoading, isError }];
};
export default useDataApi;
Still not great, but does what you're trying to do?

How can I return only the set-State from my fetched json data?

im using this Api to get json data.
const FetchEarthquakeData = url => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(jsonData => setData(jsonData.features))
}, [url]);
return data;
};
The problem is when I use this function like this:
const jsonData = FetchEarthquakeData(url)
console.log(jsonData);
I get following console.logs:
null
Array(17)
So my function FetchEarthquakeData returns the null variable and! the desired api. However if I want to map() over the jsonData, the null value gets mapped. How can I refactor my code so I get only the Array?
I'm not quite sure what useState() and setData() do. But in order to fetch the json data from the API, you can make the function as followed, then you can perform operations on the fetched data.
const url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson"
const FetchEarthquakeData = url => {
return new Promise(resolve => {
fetch(url)
.then(res => res.json())
.then(jsonData => resolve(jsonData.features))
})
}
FetchEarthquakeData(url).then(features => {
console.log(features)
// Do your map() here
})
as per the documentation of react hooks ,Only Call Hooks from React Functions Don’t call Hooks from regular JavaScript functions.
React Function Components -- which are basically just JavaScript Functions being React Components which return JSX (React's Syntax.)
for your requirement you can do as follows in your react component.
idea is to have state hook initialized with empty array then update it once json data available. the fetch logic can be moved inside useeffect .
const SampleComponent=()=>{
const [data,setData] = useState([])
useeffect (){
fetch(url).then((responsedata)=>setData(responseData) ,err=>console.log (err)
}
return (
<>
{
data.map((value,index)=>
<div key=index>{value}<div>
}
</>
)
}
if you find above fetching pattern repetitive in your app thou can use a custom hook for the same.
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
in your React component you can use like
const {data,error} = useFetch(url , options)
You have to do it in an async fashion in order to achieve what you need.
I recommend you doing it separately. In case you need the data to load when the component mounts.
Another thing is that your function is a bit confusing.
Let's assume some things:
const Example = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await fetch(url);
setData(result.features);
};
fetchData();
}, []);
return (
<div>
{console.log(data)}
{data && <p>{JSON.stringify(data, null, 2)}</p>}
</div>
);
};
I am sure that the way you are doing it is storing the data properly on data, but you can not see it on your console.log. It is a bit tricky.
You can read a bit more here => https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
Adding a bit more of complexity in case you want to handle different states like loading and error.
const Example = () => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await fetch(url);
setData(result.features);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, []);
return (
<div>
{console.log(data)}
{data && <p>{JSON.stringify(data, null, 2)}</p>}
</div>
);
};

Categories

Resources