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)
Related
I am new with react hooks, i'm trying to get info from an API but when i do the request i get 2 responses first an empty array and then the data of the API, why am i getting that empty array! , this is my first question, i'm sorry.
Thanks for helping me !
import {useState, useEffect} from 'react';
const getSlides = (API) => {
const[data,setData] = useState([]);
const getData = () =>
fetch(`${API}`)
.then((res) => res.json())
useEffect(() => {
getData().then((data) => setData(data))
},[])
return data
}
export default getSlides;
The useEffect() hook runs after the first render. Since you've initialized the data state with an empty array, the first render returns an empty array.
If you're component depends on data to render, you can always conditionally return null until your data is loaded.
Also, I recommend using an async function for api requests, it allows you to use the await keyword which makes your code easier to read. The only caveat, is that you cannot pass an async function to useEffect, instead define an async function inside your hook, and then call it.
import React, { useState, useEffect } from "react";
const API = "https://example.com/data";
const GetSlides = (props) => {
const [data, setData] = useState();
useEffect(() => {
async function getData() {
const request = fetch(API);
const response = await request;
const parsed = await response.json();
setData(parsed);
}
getData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (data === undefined) {
return null;
}
return <>data</>;
};
export default GetSlides;
Of course, you can still use Promise chaining if you desire.
useEffect(() => {
async function getData() {
await fetch(API)
.then((res) => res.json())
.then((data) => setData(data));
}
getData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
<GetSlides api="https://yay.com" />
react components need to be title case
import React, { useState, useEffect } from 'react'
const GetSlides = ({ api }) => {
const [data, setData] = useState(null)
const getData = async () =>
await fetch(`${api}`)
.then((res) => res.json())
.then((data) => setData(data))
useEffect(() => {
getData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
console.log(data)
return <div>slides</div>
}
export default GetSlides
The effect callback function is called after the render of your component. (Just like componentDidMount) So during the first render phase, the data state has not been set yet.
You initialize your data with and empty array here:
const[data,setData] = useState([] <- empty array);
useEffect runs after your component is mounted, and then calls the API, that it might take a few seconds or minutes to retrieve the data, but you return the data right away before knowing if the API finished its call.
If you want to return the data after it has been retrieved from the API, you should declare and async method
const getSlides = async (API) => {
try {
const res = await fetch(API);
const data = await res.json();
return data;
} catch (e) {
throw new Error(e);
}
}
Note that it is not necessary hooks for this function
I am using React-native and in it, I have a custom Hook called useUser that gets the user's information from AWS Amplify using the Auth.getUserInfro method, and then gets part of the returned object and sets a state variable with it. I also have another Hook called useData hook that fetches some data based on the userId and sets it to a state variable.
useUser custom-Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
export default useUserId;
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) = > { // code to fetch data from GraphQl}
const useData = () => {
const [data, setData] = useState();
useEffect(() => {
const userId = useUser();
fetchData(userId).then( // the rest of the code to set the state variable data.)
},[])
return data
}
When I try to do this I get an error telling me
*Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.*
I think the problem is that I am calling the Hook useUser inside of the use effect, but using it inside the function will cause the problem described here, and I can't use it outside the body of the fetchData since the useData itself is a hook, and it can be only used inside a functional component's or Hook's body. So I don't know how to find a way around this problem.
Correct, React hooks can only be called from React function components and other React hooks. The useEffect hook's callback isn't a React hook, it's a callback. According to the Rules of Hooks, don't call hooks inside loops, conditions, or nested functions.
I suggest refactoring the useData hook to consume the userId as an argument, to be used in the dependency array of the useEffect.
const fetchData = async (userId) => {
// code to fetch data from GraphQl
};
const useData = (userId) => {
const [data, setData] = useState();
useEffect(() => {
fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
});
}, [userId]);
return data;
};
Usage in Function component:
const userId = useUser();
const data = useData(userId);
If this is something that is commonly paired, abstract into a single hook:
const useGetUserData = () => {
const userId = useUser();
const data = useData(userId);
return data;
};
...
const data = useGetUserData();
Though you should probably just implement as a single hook as follows:
const useGetUserData = () => {
const [data, setData] = useState();
useEffect(() => {
getUserInfo()
.then(fetchData) // shortened (userId) => fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
setData(....);
});
}, []);
return data;
};
You can't call hook inside useEffect, Hook should be always inside componet body not inside inner function/hook body.
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) => {
// code to fetch data from GraphQl}
};
const useData = () => {
const [data, setData] = useState();
const userId = useUser();
useEffect(() => {
if (userId) {
fetchData(userId).then(setData);
}
}, [userId]);
return data;
};
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.
I'm trying to create a simple fetch with hooks from an AWS database. At the moment it errors out and the only reason I can see is because it breaks the rules of hooks but I'm not sure how. It's at the top level of this functional component and it's not called inside an event handler.
The result of this call (an array of user data), needs to be exported as a function and called in another file.
If anyone can spot something I have missed and can highlighted how I'm breaking the rules of hooks I'd be grateful!
Thanks!
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [Users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"USERSDATABASE"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return Users;
};
export { FetchUsers };
consumed here....
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
const mappedUsers = FetchUsers();
mappedUsers.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
The problem is that you are calling the FetchUsers inside a Class component, and the FetchUsers is executing a React Hook. This is not allowed by React.
First - Hooks don't work inside class based components.
Second - All custom hooks should start with use, in your case useFetchUsers. By setting use as prefix, react will track your hook for deps and calling in correct order and so on.
I'm trying to transition a small example from using shouldComponentUpdate to using React hooks. I also want to write some unit tests within Jest, which is where I'm struggling.
Note: that I've seen some existing questions/answers and I do not want to use a setTimeout for my testing.
The code I have at the moment looks like this, firstly the component:
export default function HelloStarWars(props) {
const data = useFetch(`https://swapi.co/api/people/${props.id}`);
const { name } = data;
return (
<h1>Hello {name || "..."}!</h1>
);
}
where useFetch is defined as follows:
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState({});
async function getData() {
const response = await fetch(url);
const data = await response.json();
setData(data);
}
useEffect(() => {
getData();
}, []);
return data;
}
Now the actual bit I'm struggling with, is I can't work out how to convert this code that I put together. Essentially I have no componentDidMount() function to await anymore - so what do I wait upon to ensure that the data has been retrieved?
describe("ComponentDidMount", () => {
beforeEach(() => {
fetch.resetMocks();
});
it("Obtains result and sets data correctly", async () => {
const wrapper = await mount(<HelloStarWars id="1" />);
const instance = wrapper.instance();
fetch.mockResponseOnce(JSON.stringify({ name: 'Jedi Master' }));
await instance.componentDidMount();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
One option I'm aware of is I could, spy on the useFetch and mock the return result. I will do that if I have to, but at the moment I like the jest-fetch-mock library that I'm using and the capabilities it offers.