Setting state along with AsyncStorage in useEffect hook causes infinite loop? - reactjs

I'm new to hooks and recently started using hooks in my React Native projects.
I'm building a simple todo app using the AsyncStorage. First I initialize initial data and setData state using useState hook:
const [data, setData] = useState([]);
There are two textInput and submit button that I use to save data to AsyncStorage. Here is the saveData function:
const saveData = async () => {
const arrData = [{ name: 'vikrant', phone: 123456 }]; // [{ name, phone}] from the textInput
const storedData = await AsyncStorage.getItem('user');
const storedDataParsed = JSON.parse(storedData);
let newData = [];
if (storedData === null) {
// save
await AsyncStorage.setItem('user', JSON.stringify(arrData));
} else {
newData = [...storedDataParsed, user];
await AsyncStorage.setItem('user', JSON.stringify(newData));
}
setName('');
setPhone('');
Keyboard.dismiss();
};
Now, I'm using useEffect to get data from the AsyncStorage and setting it to the data state. I'm using data to render the text in the screen.
useEffect(() => {
retrieveData();
}, [data]);
const retrieveData = async () => {
try {
const valueString = await AsyncStorage.getItem('user');
const value = JSON.parse(valueString);
setData(value);
} catch (error) {
console.log(error);
}
};
I'm using [data] in useEffect since I want to re-render my component each time data changes i.e. each time I save data in AsyncStorage. But this is causing infinite loop as setData causes useEffect to run infinitely.
If I remove data from the [] it doesn't loop but my data in render is one step behind. So whenever I save data it doesn't show the current data but the previous one.
Any explanation of what I am doing wrong here and how can i fix this?
Thanks.

As already mentioned by you, the infinite loop is due to thefact that you pass data as a dependency to useEffect and also set in inside the function called in useEffect.
The solution here is to not use useEffect and instead setData whenever you are setting value in AsyncStorage
const saveData = async () => {
const arrData = [{ name: 'vikrant', phone: 123456 }]; // [{ name, phone}] from the textInput
const storedData = await AsyncStorage.getItem('user');
const storedDataParsed = JSON.parse(storedData);
let newData = [];
if (storedData === null) {
// save
await AsyncStorage.setItem('user', JSON.stringify(arrData));
} else {
newData = [...storedDataParsed, user];
await AsyncStorage.setItem('user', JSON.stringify(newData));
}
setName('');
setPhone('');
setData(newData);
Keyboard.dismiss();
};

Just add a conditional flag, retrieve to wrap async storage, retrieveData(), calls.
Also in the context of "saving data" I would probably just separate async storage-ish logic with state logic. Current saveData is polluted with both state and async storage logic.
Something like:
const [retrieve, setRetrieve] = useState(false);
// Pure AsyncStorage context
const saveData = async () => {
...
if (storedData === null) {
await AsyncStorage.setItem('user', JSON.stringify(arrData));
} else {
newData = [...storedDataParsed, user];
await AsyncStorage.setItem('user', JSON.stringify(newData));
}
// XXX: Removed state logic, call it somewhere else.
};
const someHandler = async () => {
await saveData();
setRetrieve(true); // to signal effect to call retrieveData()
}
Then the goal of the effect is just to run retrieveData() once saving is done.
const [data, setData] = useState([]);
useEffect(() => {
const retrieveData = async () => {
try {
const valueString = await AsyncStorage.getItem('user');
const value = JSON.parse(valueString);
// Other set states
setData(value);
} catch (error) {
console.log(error);
}
};
// Retrieve if has new data
if (retrieve)
retrieveData();
setRetrieve(false);
}
}, [retrieve]);

Related

Typescript with React: Using custom hook in useEffect

I'm trying to use a hook inside of a useEffect call to run only once (and load some data).
I keep getting the error that I can't do that (even though I've done the exact same thing in another app, not sure why 1 works and the other doesn't), and I understand I may be breaking the Rules of Hooks... so, what do I do instead? My goal was to offload all the CRUD operation logic into a simple hook.
Here's MenuItem, the component trying to use the hook to get the data.
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
const [menu, setMenu] = useState<Item[]>([]);
const { getMenu, retrievedData } = useMenu();
//gets menu items using menu-hook
useEffect(() => {
getMenu();
}, []);
//if menu is retrieved, setMenu to retrieved data
useEffect(() => {
if (retrievedData.length) setMenu(retrievedData);
}, []);
//onClick of menu item, displays menu item description
const itemHandler = (item: Item) => {
if (ID === null || ID !== item._id) {
setID(item._id);
} else {
setID(null);
}
};
return ...
};
And here's getMenu, the custom hook that handles the logic and data retrieval.
const useMenu = () => {
const backendURL: string = 'https://localhost:3001/api/menu';
const [retrievedData, setRetrievedData] = useState<Item[]>([]);
const getMenu = async () => {
await axios
.get(backendURL)
.then((fetchedData) => {
setRetrievedData(fetchedData.data.menu);
})
.catch((error: Error) => {
console.log(error);
setRetrievedData([]);
});
};
return { getMenu, retrievedData };
};
export default useMenu;
And finally here's the error.
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'd like to add I'm also using Typescript which isn't complaining right now.
There's a few things you can do to improve this code, which might help in future. You're right that you're breaking the rule of hooks, but there's no need to! If you move the fetch out of the hook (there's no need to redefine it on every render) then it's valid not to have it in the deps array because it's a constant.
I'd also make your useMenu hook take care of all the details of loading / returning the loaded value for you.
const fetchMenu = async () => {
const backendURL: string = 'https://localhost:3001/api/menu';
try {
const { data } = await axios.get(backendURL);
return data.menu;
} catch (error: AxiosError) {
console.log(error);
return [];
};
}
export const useMenu = () => {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
fetchMenu.then(result => setItems(result);
}, []);
return items;
};
Now you can consume your hook:
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
// Now this will automatically be an empty array while loading, and
// the actual menu items once loaded.
const menu = useMenu();
// --- 8< ---
return ...
};
A couple of other things -
Try to avoid default exports, because default exports are terrible.
There are a lot of packages you can use to make your life easier here! react-query is a good one to look at as it will manage all the lifecycle/state management around external data
Alternatively, check out react-use, a collection of custom hooks that help deal with lots of common situations like this one. You could use the useAsync hook to simplify your useMenu hook above:
const backendURL: string = 'https://localhost:3001/api/menu';
const useMenu = () => useAsync(async () => {
const { data } = await axios.get(backendURL);
return data.menu;
});
And now to consume that hook:
const MenuItem = () => {
const { value: menu, loading, error } = useMenu();
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <>The menu could not be loaded</>;
}
return ...
};
As well as being able to display a loading indicator while the hook is fetching, useAsync will not give you a memory leak warning if your component unmounts before the async function has finished loading (which the code above does not handle).
After working on this project for some time I've also found another solution that is clean and I believe doesn't break the rule of hooks. This requires me to set up a custom http hook that uses a sendRequest function to handle app wide requests. Let me make this clear, THIS IS NOT A SIMPLE SOLUTION, I am indeed adding complexity, but I believe it helps since I'll be making multiple different kinds of requests in the app.
This is the sendRequest function. Note the useCallback hook to prevent unnecessary rerenders
const sendRequest = useCallback(
async (url: string, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) throw new Error(responseData.message);
setIsLoading(false);
return responseData;
} catch (error: any) {
setError(error);
setIsLoading(false);
throw error;
}
},
[]
);
Here's the new useMenu hook, note I don't need to return getMenu as every time sendRequest is used in my app, getMenu will automatically be called.
export const useMenu = () => {
const { sendRequest } = useHttpClient();
const [menu, setMenu] = useState<MenuItem[]>([]);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const getMenu = async () => {
try {
const responseData = await sendRequest(`${config.api}/menu`);
setMenu(responseData.menu);
setMessage(responseData.message);
} catch (error) {}
};
getMenu();
}, [sendRequest]);
return { menu, message };
};
Good luck

Need router.query variable passed to API URL call, but is undefined on first render

I have the following API Call:
const router = useRouter();
const { albumQuery } = router.query;
const [albums, setAlbums] = useState([]);
const fetchAlbumsHandler = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const url = `http://ws.audioscrobbler.com/2.0/?method=album.search&album=${albumQuery}&api_key=MY_API_KEY&format=json`;
const res = await fetch(url);
const data = await res.json();
if (!res.ok) {
throw new Error("Something went wrong!");
}
const jsonAlbums = data.map(
// JSON business Logic
);
setAlbums(transformedAlbums);
} catch (error) {
setError(error.message);
}
setIsLoading(false);
}, []);
With the corresponding useEffect function:
useEffect(() => {
fetchAlbumsHandler();
}, [fetchAlbumsHandler]);
However, the API call takes ${albumQuery} as undefined on the first render due to NextJS implementation details. Is there a way for me to access the variable on the first render?
No, if the albumQuery isn't available on the initial render then the code should handle waiting for it to become available.
The existing code is assuming albumQuery is available on the initial render and attempts to close it over in the useCallback hook. After this the useEffect hook is called and since fetchAlbumsHandler is now a stable reference the useEffect hook won't be retriggered nor will fetchAlbumsHandler be re-memoized since the useCallback hook has an empty dependency array.
Minimally albumQuery appears to be a dependency for the useCallback hook and/or the useEffect hook. If fetchAlbumsHandler isn't passed as a prop to children there's no real benefit to memoizing it. I suggest moving it into the useEffect hook callback and using albumQuery as a dependency.
Example:
const router = useRouter();
const { albumQuery } = router.query;
const [albums, setAlbums] = useState([]);
useEffect(() => {
const fetchAlbumsHandler = async () => {
setIsLoading(true);
setError(null);
try {
const url = `http://ws.audioscrobbler.com/2.0/?method=album.search&album=${albumQuery}&api_key=MY_API_KEY&format=json`;
const res = await fetch(url);
const data = await res.json();
if (!res.ok) {
throw new Error("Something went wrong!");
}
const jsonAlbums = data.map(
// JSON business Logic
);
setAlbums(transformedAlbums);
} catch (error) {
setError(error.message);
}
setIsLoading(false);
}
fetchAlbumsHandler();
}, [albumQuery]);

How to async fetch data in useEffect

I have some issue. When I do to async fetch data (using axios for fetching) in the useEffect, and after I set responsed data to state, using a useState hook. And page render befor then I got response from server.
For demonstration this issue I have putted console.log for get current state, and I get 'undefined':
const [positions, setPositions] = useState([]);
useEffect(() => {
const fetchPositions = async () => {
const response = await EmployeeService.getEmployeePositions();
setPositions(response.data);
};
fetchPositions();
console.log('positions from state: ', positions); //undefined
}, []);
Method for fetching data from "EmployeeService":
getEmployeePositions(){
return axios.get(EMPLOYEE_API_BASE_URL + '/positions');
}
Thanks in advance, and best regards!
React needs to re-render to display the results.
Which means you need to capture the result on the subsequent re-render that is caused when you setState.
Move the console log outside of the useEffect
const [positions, setPositions] = useState([]);
useEffect(() => {
const fetchPositions = async () => {
const response = await EmployeeService.getEmployeePositions();
setPositions(response.data);
};
fetchPositions();
}, []);
console.log('positions from state: ', positions); // NOT UNDEFINED
React will always render once before you have data.
So you can catch it with a condition.
if (positions.length === 0) {
return null;
}
nothing wrong with your code, useEffect is always undefined because it read the first value of your rendered app.
To update state in useEffect put paramater on the array [] but in your case it will cause an infinity loop.
try logging inside the async function instead
const [positions, setPositions] = useState([]);
useEffect(() => {
const fetchPositions = async () => {
const response = await EmployeeService.getEmployeePositions();
setPositions(response.data);
console.log('data from response: ', response);
};
fetchPositions();
}, []);
or do it like this
const [positions, setPositions] = useState([]);
useEffect(() => {
const fetchPositions = async () => {
const response = await EmployeeService.getEmployeePositions();
setPositions(response.data);
console.log('data from response: ', response);
};
if((positions ?? []).length == 0){
fetchPositions();
console.log('this is position state before fetch~>',positions)
} else{
console.log('this is position state after fetch~>',positions)
}
}, [positions]);

State coming back undefined from useEffect

I am trying to setState from useEffect but it comes back as undefined and I am unable to use it in other components. I am able to console log the state though and it displays the object fine. Thanks.
function App() {
const [tokens, setTokens] = useState();
console.log(tokens)
useEffect(() => {
async function init() {
await Moralis.initPlugins();
await Moralis.enable();
await listAvailableTokens();
}
init();
// token info from 1inch
const listAvailableTokens = async () => {
const result = await Moralis.Plugins.oneInch.getSupportedTokens({
chain: "eth", // The blockchain you want to use (eth/bsc/polygon)
});
const tokensObject = result.tokens;
console.log(tokensObject)
setTokens(tokensObject);
};
}, []);

How can I make react's useEffect to stop rerender in an infinite loop even with a dependency specified?

I am working on a small CRUD fullstack app with react and mongodb and I have this problem where I use useEffect to make an axios get request to the server to get all of my todos. The problem is that useEffect does it's job but it also rerenders to infinity. This is my component:
export default function () {
...
const [todos, setTodos] = useState([]);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
async function populateTodos () {
try {
const res = await axios.get(`http://localhost:8000/api/all-todos/${currentUser}`);
setTodos(res.data);
} catch (err) {
if (err.response) {
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
} else if (err.request) {
console.log(err.request);
} else {
console.log('Error: ', err.message);
}
}
}
populateTodos();
}, [todos]);
console.log(todos);
return (
...
);
}
So what I was expecting to happen is that that console.log to get printed only when the todos changes, like when I add a new todo and so on, but instead it gets printed forever.
You said that you need to fetch todos at first, and whenever todos change. I can suggest you a different approach, using one more variable, something like this:
const TodosComponent = (props) => {
const [todos, setTodos] = useState([]);
const [updatedTodos, setUpdatesTodos] = useState(true);
const fetchFunction = () => {
// In here you implement your fetch, in which you call setTodos().
}
// Called on mount to fetch your todos.
useEffect(() => {
fetchFunction();
}, []);
// Used to updated todos when they have been updated.
useEffect(() => {
if (updatedTodos) {
fetchFunction();
setUpdatesTodos(false);
}
}, [updatedTodos]);
// Finally, wherever you update your todos, you also write `updateTodos(true)`.
}

Resources