Dynamically load from API SVG via hooks ReactJs - javascript

I have a module that renders an svg. Before it, the module should check on authorization and if ok fetch the file from api via call with a token.
I have the next code
function App() {
const [tableColors, setTableColors] = useState(["gray"]);
const [svg, setSvg] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [isErrored, setIsErrored] = useState(false);
new AuthService().getUser().then(user =>{ if(!user) {
Login()
}
else{
useEffect( async () => {
LoadSvg()
.then(res => res.text())
.then(setSvg)
.catch(setIsErrored)
.then(() => setIsLoaded(true))
}, [])
}})
return (
<div className="App">
<header className="App-header">
<SvgLoader svgXML={svg}>
</SvgLoader>
</header>
</div>
);
function Login(){
var a = new AuthService();
a.login();
}
async function LoadSvg(){
return await new ApiService().callApi("https://localhost:44338/api/v1/desk-booking/Plan/getroomplanbyid?roomId=1")
}
}
And the problem I have here is "React Hook "useEffect" cannot be called inside a callback" but without using "useEffect" it fetchs the svg endlessly.
How I can solve the issue?

You are not doing it right, if you do this the "react" way then the solution would look like this
....
function App() {
const [tableColors, setTableColors] = useState(['gray']);
const [svg, setSvg] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [isErrored, setIsErrored] = useState(false);
// state to check if user is loaded, used for svg call
const [userLoaded, setUserLoaded] = useState(false);
// useEffect does not prefer async function
useEffect(() => {
if (!userLoaded) {
new AuthService().getUser().then(user => {
if (!user) {
Login();
// indicate user was loaded
// I would move the login function body here instead since, login is async, so this might not work as intended but you get the idea
setUserLoaded(true);
}
});
}
}, [])
useEffect(() => {
// if userLoaded is true then go ahead with svg loading
if (userLoaded) {
LoadSvg()
.then(res => res.text())
.then(setSvg)
.catch(setIsErrored)
.then(() => setIsLoaded(true));
}
// Add svg useEffect dependency on userLoaded
}, [userLoaded]);
......
Note, this solution is intended to give you an idea of how to do it, It might not work if you copy-paste the code.

Related

I tried this code i already passed dependency in useEffect but the api call again and again after 3/4 second

Below code i try to make get response from api and put in Movie Component. but the problem is that api hit again and again i don't why this happened.here screen shot of api call
const [loading, setloading] = useState(false);
const [movielist, setmovielist] = useState([]);
const [err, seterr] = useState("");
const fetchMoviesHandler = useCallback(async () => {
setloading(true);
try {
const reponse = await fetch("https://swapi.dev/api/films");
if (reponse.status != 200) {
seterr("something is wrong");
}
const data = await reponse.json();
const result = data.results;
setmovielist(result);
} catch (errr) {
}
});
useEffect(() => {
fetchMoviesHandler();
}, [fetchMoviesHandler]);
return (
<div>
{movielist.map((movie) => {
return <Movie key={movie.episode_id} title={movie.title} />;
})}
</div>
);
};
This is returning a new instance of the function on every render:
const fetchMoviesHandler = useCallback(async () => {
// your function
});
Which will trigger the useEffect on every render, since this function is in its dependency array.
To tell useCallback to memoize the function and keep the same instance across multiple renders, it also needs a dependency array. For example:
const fetchMoviesHandler = useCallback(async () => {
// your function
}, [setloading, setmovielist, seterr]);
Or, at the very least, an empty dependency array:
const fetchMoviesHandler = useCallback(async () => {
// your function
}, []);
Which would essentially create one instance of the function and always use it for the life of the component.

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?

React higher order function to return hook

Currently, I have a custom fetching data hook written in javascript and it works
import {useState, useEffect} from 'react';
const useApi = apiName => id => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
Then I can use pass in what api I want to call to get the hook. For examples:
const useCustomer = useApi("customer")
const useHello = useApi("hello")
.....
const {response, loading, error} = useCustomer("id_1")
It works fine.
Then, I try to convert to typescript
const useApi = (apiName:string) => (id?:string) => {
const [response, setResponse] = useState({})
.......
}
and eslint complains that
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I would like to know whats wrong with this approach, I know I can have something like:
const useApi = (apiName:string, id?:string) => {}
or disable the eslint(react-hooks/rules-of-hooks)
But just curious about whats the potential problems having higher order function of hook since it actually return the response .
Thanks
When you name you function with prefix hooks, eslint thinks of it as a custom hook according to the general convention. Now that implements useState in a nested function which is why it gives you an error
The best way to write the above code is to not use currying function but pass in the apiName as a param directly
const useApi = (apiName, id) => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
and use it like
.....
const {response, loading, error} = useApi("customer","id_1");
P.S. Hooks are meant to be an alternative to HOC's and there is no point writing a hook if you use it as an HOC itself
There's a much easier way if you don't need the id variable to be in the hook. The reason why you get the warning is because your hooks are in your CB instead of your root function.
Correct Example:
const useApi = (apiName:string) => {
const [response, setResponse] = useState({});
return (id?: string) => {
.......
};
}

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

Function to get data keep running in loop

I have a function that gets some data from my backend and then I want simply to assign it to the state and display it in my browser. Everything works correctly, but I don't know why when I run a request the function keeps calling the API without stopping. What is the reason for this?
It seems that the function is stuck in some kind of while-true loop.
function App() {
const [orders, setOrders] = useState();
const getOrders = async () => {
const response = await axios.get("/api/orders/");
setOrders(response);
console.log(response);
};
getOrders();
return <div className="App">{JSON.stringify(orders)}</div>;
}
export default App;
What is the reason for this?
This happens because you are calling a function every render inside the functional component.
const getOrders = async () => {
const response = await axios.get("/api/orders/");
setOrders(response); // this will re render the component
console.log(response);
};
getOrders(); // this will be called every render and cause the infinity loop
When you render the component, you call getOrders and this functions calls setOrders wich will rerender the component, causing a infinity loop.
First render => call getOrders => call setOrders => Rerender =>
Second render => call getOrders => call setOrders => Rerender =>
...
You need to use useEffect hook or call the function on some event (maybe button click)
e.g. using useEffect
function App() {
const [orders, setOrders] = useState(null);
useEffect(() => {
const getOrders = async () => {
const response = await axios.get("/api/orders/");
setOrders(response);
console.log(response);
};
getOrders();
}, []);
return <div className="App">{JSON.stringify(orders)}</div>;
}
You want to use the Effect hook React offers:
https://reactjs.org/docs/hooks-effect.html
function App() {
const [orders, setOrders] = useState();
const getOrders = async () => {
const response = await axios.get("/api/orders/");
setOrders(response);
console.log(response);
};
useEffect(() => {
getOrders();
}, []); //Empty array = no dependencies = acts like componentDidMount / will only run once
return <div className="App">{JSON.stringify(orders)}</div>;
}
export default App;
I think you are using react-hooks here, so your getOrders function is getting render all the time.
Use useEffect from react to avoid this.
function App() {
const [orders, setOrders] = useState();
useEffect(() => {
const getOrders = async () => {
const response = await axios.get("/api/orders/");
setOrders(response);
console.log(response);
};
getOrders();
}, [])
return <div className="App">{JSON.stringify(orders)}</div>;
}
export default App;

Categories

Resources