From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.
Related
Hello i'm newbie here...
I found my friend's code when he using useState instead of using useEffect to fetch the API.
I tried it and it worked, the code didn't cause an error and infinite loops.
here is for the code
import { useState } from "react";
import { IN_THEATER, POSTER } from "../../../constant/movies";
import { GlobalGet } from "../../../utilities/fetch";
const Service = () => {
const [movieData, setMovieData] = useState({ data: null, poster: null });
const fetchMovieData = async () => {
try {
let movieRes = await GlobalGet({ url: `${IN_THEATER}` });
return movieRes;
} catch (error) {
console.log(error);
}
};
const fetchPoster = async () => {
try {
let posterRes = await GlobalGet({ url: `${POSTER}` });
return posterRes;
} catch (error) {
console.log(error);
}
};
const fetchData = async () => {
setMovieData({
data: await fetchMovieData(),
poster: await fetchPoster(),
});
};
useState(() => { //<=here it is
fetchData();
}, []);
return {
movieData,
};
};
export default Service;
And my question is, why it could be happen ? why using useState there doesn't cause an infinite loops ?
The useState() function can accept an initializer function as its first argument:
const [state, setState] = useState(initializerFunction)
When a function is passed to useState(), that function is only called once before the component initially mounts. In your case below, the initializer function is an anonymous arrow function:
useState(() => { // <-- Initializer function invoked once
fetchData();
}, []);
Here, the initializer function is () => { fetchData(); }, which is invoked once before the initial mount, so the fetchData() method is only called once. The array that is passed as the second argument [] is useless and doesn't do anything in this case as it's ignored by useState(). The above useState would behave the same if you did useState(fetchData);. Because fetchData() is only called once on the initial mount, any state updates of your component don't cause the fetchData() function to execute again as it's within the initializer function.
With that said, useState() shouldn't be used for fetching data on mount of your component, that's what useEffect() should be used for instead.
Generally it's possible to fetch data from outside of the useEffect hook.
Somewhere in the body of your component...
const [fetchedData, setFetchedData] = useState(false)
const someFetchFunc = asyunc (url) => {
setFetchedData(!fetchedData)
const res = await fetch(url)
const data = await res.json()
setMovieData(data)
}
!fetchedData && someFetchFunc()
But this is an antipattern. In this case developer lacks a whole toolset of dealing with possible issues. What if fetching fails?
So, it's generally a good idea to handle all the side effects like fetching in a place that was intended for that. It's useEffect hook)
I'm trying to fetch the advertisers list when the component mounts, but it is causing me memory leak,
Any suggestions ? i'll be grateful :)
useEffect(() => {
const fetchData = async () => {
const list = await Api.listAdvertisers();
setAdvertisers(list);
};
fetchData();
}, []);
You can declare your fetchData function on main scope and you can check the function if it is called or not by declaring a didMount variable in useEffect scope.
const didMount = useRef(false);
const fetchData = async () => {
const list = await beeswaxService.listAdvertisers();
setAdvertisers(list);
return;
};
useEffect(() => {
if(!didMount.current){
didMount.current = true;
fetchData();
}
}, []);
Maybe the function inside the useEffect is causing memory leak, try this?
const fetchData = async () => {
const list = await beeswaxService.listAdvertisers();
setAdvertisers(list);
return;
};
useEffect(() => {
fetchData();
}, []);
I onpress method triggered the pressed useEffect api using promise in useEffect and according to incoming data set myDataStt and assign data to getData function I want to take action accordingly but I logged to getData func. triggered useEffect myDataStt wont change, press the buton second time this time succes myDataStt is change but first time press the button wont work (use effect triggered and value is correct) I dont understand why, thanks for comments
const [pressed, setPressed] = useState(false);
const [myDataStt, setMyData] = useState(announcements);
useEffect(() => {
CallApi.then(
values => {
setMyData(values);
const data = getData();
}),
}, [pressed]);
const getData = () => {
return myDataStt.dataFirst || [];
};
<Button
onPress={() => {
setPressed(true);
}}>
You shouldn't use useEffect in manner. Have your button actually perform the API call. If you want the API to load data on the component mount, then have it called in useEffect and on click. There is no need for a pressed state. You can assign your function to a callback, so it doesn't trigger an infinite loop.
const getData = useCallback(async () => {
const values = await CallApi();
setMyData(values);
}, [setMyData]);
useEffect(()=> {
getData();
}, [getData])
<Button onClick={() => getData()}>Refresh API</Button>
Look if this complete component works for you.
import React, { useState, useEffect } from 'react';
const CallAPI = () => {
//do your request/fetch
};
export const Component = () => {
const [data, setData] = useState([]);
useEffect(() => {
const initialCall = async () => {
const values = await CallAPI();
setData(values);
};
initialCall();
return () => {
//clean your fetch
};
}, []);
const handleClick = async () => {
const values = await CallAPI();
setData(values);
};
return (
<>
<button onClick={handleClick}>Add</button>
{data.map(item => {
return <div>{item}</div>;
})}
</>
);
};
2 remarks, you dont need 2 states for call your api, try to use onClick in buttons.
I am trying to test custom hook. I want to know is setState function fire or not.
here is my custom hook
import React from "react";
import axios from "axios";
export default () => {
const [state, setState] = React.useState([]);
const fetchData = async () => {
const res = await axios.get("https://5os4e.csb.app/data.json");
setState(res.data);
};
React.useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
return { state };
};
now I am trying to test this custom hook. I want to know is setState function fire or not .
I tried like this
import moxios from "moxios";
import React from "react";
import { act, renderHook, cleanup } from "#testing-library/react-hooks";
import useTabData from "./useTabData";
describe("use tab data", () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
describe("non-error response", () => {
// create mocks for callback arg
const data = [
{
name: "hello"
}
];
let mockSetCurrentGuess = jest.fn();
beforeEach(async () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
});
});
test("calls setState with data", async () => {
React.useState = jest.fn(() => ["", mockSetCurrentGuess]);
const { result, waitForNextUpdate } = renderHook(() => useTabData());
console.log(result);
//expect(mockSetCurrentGuess).toHaveBeenCalledWith(data);
});
});
});
You should not mock the React internals. This is incorrect. Either ways, this code has no effect in mocking due to closures. Even if it worked, no point in testing if you are mocking the real implementation, isn't it ? :)
I would recommend to try to get grasp of what react hook is doing in your code.
You have a state in your custom hook:
const [state, setState] = React.useState([]);
.
.
return [state]; //you are returning the state as ARRAY
#testing-library/react-hooks allows you to debug and get value of current outcome of hook.
const { result, waitForNextUpdate } = renderHook(() => useTabData());
const [foo] = result.current; // the array you returned in hook
expect(foo).toEqual('bar'); //example assertion
I would stop here and allow you to learn and debug.
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;