I have a useEffect() that checks a trigger boolean in state if there is a message sound that should play, and after playing it sets that active message trigger to false in state.
However, the useEffect() goes into an infinite loop crashing the app. Probably because changing the state triggers it again (and again...)
Usually, with useState, this is fairly simple to fix with something like useEffect(() => {logic}, [trigger])
In my case I am not using useState, but I am using a reducer to change state.
Edit: The weird thing is, the reducer sometimes works to modify state, and sometimes it does not. It will execute without errors but the state remains unchanged.
Let me show you my commented code:
import React, { useEffect } from "react";
import { getCachedImage } from "../helpers";
const MessageNotification = (props) => {
const messageImg= getCachedImage("/ui/newmessage.png");
// Function that plays given sound
function playSound(soundFile) {
let audio = new Audio("/audio/messages/" + soundFile);
audio.play();
}
// Check if a Message is set to active. If so, execute logic
useEffect(() => {
// Get messages from state and find the message with "active" set to true
const messagesState = props.state.messages;
const activeMessage = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
// Mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
});
return (
<div>
<img src={"images" + messageImg} alt="message" width="90" height="90"></img>
</div>
);
};
export default MessageNotification;
As you can see, I do not use useState but work with a reducer instead. The solution that I often find which pertains to something like the following is not my solution as far as I can tell:
// Not applicable solution for me, since I use reducer
const [trigger] = useState();
useEffect(() => {
// Logic here
}, [trigger]);
Edit: Since the reducer does not seem to modify state when used in useEffect, let me post its code:
const reducer = (state, action) => {
switch (action.type) {
case "UPDATE_MESSAGES":
return { ...state, messages: action.payload };
default:
throw new Error();
}
};
export default reducer;
Try adding a dependency for your useEffect, such as:
useEffect(() => {
if (activeMessage) {
playSound(activeMessage.audio);
//mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
}, [activeMessage]);
By not specifying a dependency array, your useEffect will run on EVERY render, hence creating an infinite loop.
Also, you are trying to directly modify a prop (and it is an anti pattern) on this line:
const messagesState = props.state.messages;
Try changing it to this:
const messagesState = [...props.state.messages];
Also, let index = messagesState.indexOf(activeMessage); will not work since messagesState is an object array. To get the index of the active message, try this:
let index = messagesState.map(message => message.active).indexOf(true);
I think if you add props.state.messages as dependency, the problem will fixed. Also if you use only the messagesState and messagesState in useEffect, you should move this variables to that block:
useEffect(() => {
const messagesState = props.state.messages;
const messagesState = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
//mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
}, [props.state.messages]);
// Check if a Message is set to active. If so, execute logic
useEffect(() => {
// Get messages from state and find the message with "active" set to true
const messagesState = props.state.messages;
const activeMessage = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
// Mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
});
your useEffect needs a dependency, if you are not providing dependency in useEffect like in your case it'll always run on every render. Provide [] as second argument in your useEffect or [any state or prop on which this effect depends].
Related
I am trying to move my if-statement [line 30] located in useFetchMovieGenreResults [inside the hooks folder] from outside my my useEffect to inside the useEffect. However when I do this, I am not receiving my expected output (an object), rather I receive the error below.
However, when I move my if-Statement outside of my useEffect I get my expected output after 2 renders.
So my question is,
why is it when I move my If-Statement inside my useEffect, I do'nt
get back the console.log results but I would get them back when I would move my if-Statement.
How do I reduce the number of renders I need to get my my result to
one render?
I have linked to the code sandbox of the problem below so that you could get a bigger picture of the problem and see the app in its entirety. However below that is an example of what I have compared to what I am looking for.
https://codesandbox.io/s/billowing-frog-hr4nwd
What I have and is working. Notice the If-Statement outside the useEffect
import useFetchNavBarCatagories from "./useFetchNavBarCatagories";
import useSelectedGenreInfoExtractor from "./useSelectedGenreInfoExtractor";
import { useEffect, useState } from "react";
export default function useFetchMovieGenreResults(genre) {
const [mymoviegenreinfo, setMymoviegenreinfo] = useState();
const [mymoviegenreinfofinal, setMymoviegenreinfofinal] = useState();
const mymoviegenreobjects = useFetchNavBarCatagories();
console.log("Checking if its reading", genre);
useEffect(() => {
setMymoviegenreinfo(mymoviegenreobjects);
}, [mymoviegenreobjects]);
if (mymoviegenreinfo?.genres?.length > 0) {
//! The bottom maps the key's values {genre[]}. Basically removing the outter object
var mappedvalues = Object.keys(mymoviegenreobjects).map(
(e) => mymoviegenreobjects[e]
);
console.log("The latest testinggggggggggggggggggg", mappedvalues);
const flatarrayofvalues = mappedvalues.flat();
console.log("Finally flat", flatarrayofvalues);
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
console.log("This is avangers two ID", filtteredarray[0].id);
console.log("just a check", mymoviegenreinfo);
console.log(
"a check for lengthhhhhhhhhhhhhhhhhh",
mymoviegenreinfo?.genres?.length
);
return mymoviegenreinfo;
}
}
What I am looking for.
Notice the if-Statement is inside the useEffect. However when I do it like this, my if-statement never runs because none of my judging by the fact that my console.logs don't run even when my conditions for the if-statement are fulfilled.
import useFetchNavBarCatagories from "./useFetchNavBarCatagories";
import useSelectedGenreInfoExtractor from "./useSelectedGenreInfoExtractor";
import { useEffect, useState } from "react";
export default function useFetchMovieGenreResults(genre) {
const [mymoviegenreinfo, setMymoviegenreinfo] = useState();
const [mymoviegenreinfofinal, setMymoviegenreinfofinal] = useState();
const mymoviegenreobjects = useFetchNavBarCatagories();
console.log("Checking if its reading", genre);
useEffect(() => {
setMymoviegenreinfo(
mymoviegenreobjects,
console.log("This is my infoooooo from the setstae", mymoviegenreinfo)
);
if (mymoviegenreinfo?.genres?.length > 0) {
//! The bottom maps the key's values {genre[]}. Basically removing the outter object
var mappedvalues = Object.keys(mymoviegenreobjects).map(
(e) => mymoviegenreobjects[e]
);
console.log("I said HELLLLO0000");
console.log("The latest testinggggggggggggggggggg", mappedvalues);
const flatarrayofvalues = mappedvalues.flat();
console.log("Finally flat", flatarrayofvalues);
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
console.log("This is avangers two ID", filtteredarray[0].id);
console.log("just a check", mymoviegenreinfo);
console.log(
"a check for lengthhhhhhhhhhhhhhhhhh",
mymoviegenreinfo?.genres?.length
);
setMymoviegenreinfofinal(filtteredarray);
console.log(filtteredarray);
}
}, [mymoviegenreobjects]);
return mymoviegenreinfo;
}
I think you are putting if statement wrong place you can have multiple useEffect() in react so you can put that code another one or inside useEffect() after setMymoviegenreinfo(mymoviegenreobjects);
inside the fat arrow function body.
Instead of using a separate state for a computed object, use useMemo. Don't use state in the same function you assign using set, as it will cause an unresolved loop.
const mymoviegenreinfofinal = useMemo(() => {
if (mymoviegenreinfo?.genres?.length > 0) {
var mappedvalues = Object.keys(mymoviegenreinfo).map(
(e) => mymoviegenreinfo[e]
);
const flatarrayofvalues = mappedvalues.flat();
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
return filtteredarray;
}, [mymoviegenreinfo])
I am trying to figure out how to solve the following problem in the best way possible:
I have multiple components all requiring a global state (I am using recoil for this, since I have many different "atom" states).
Only if a component gets loaded that requires that state, it will perform an expensive computation that fetches the data. This should happen only once upon initialisation. Other components that require the same piece of data should not re-trigger the data fetching, unless they explicitly call an updateState function.
Ideally, my implementation would look something like this:
const initialState = {
uri: '',
balance: '0',
};
const fetchExpensiveState = () => {
uri: await fetchURI(),
balance: await fetchBalance(),
});
const MyExpensiveData = atom({
key: 'expensiveData',
default: initialState,
updateState: fetchExpensiveState,
});
function Component1() {
const data = useRecoilMemo(MyExpensiveData); // triggers `fetchExpensiveState` upon first call
return ...
}
function Component2() {
const data = useRecoilMemo(MyExpensiveData); // doesn't trigger `fetchExpensiveState` a second time
return ...
}
I could solve this by using useRecoilState and additional variables in the context that tell me whether this has been initialised already, like so:
export function useExpensiveState() {
const [context, setContext] = useRecoilState(MyExpensiveData);
const updateState = useCallback(async () => {
setContext({...fetchExpensiveState(), loaded: true});
}, []);
useEffect(() => {
if (!context.loaded) {
setContext({...context, loaded: true});
updateState();
}
}, []);
return { ...context, updateState };
}
It would be possible to make this solution more elegant (not mixing loaded with the state for example). Although, because this should be imo essential and basic, it seems as though I'm missing some solution that I haven't come across yet.
I solved it first by using a loaded and loading state using more useRecoilStates. However, when mounting components, that had other components as children, that all used the same state, I realized that using recoil's state would not work, since the update is only performed on the next tick. Thus, I chose to use globally scoped dictionaries instead (which might not look pretty, but works perfectly fine for this use case).
Full code, in case anyone stumbles upon a problem like this.
useContractState.js
import { useWeb3React } from '#web3-react/core';
import { useEffect, useState } from 'react';
import { atomFamily, useRecoilState } from 'recoil';
const contractState = atomFamily({
key: 'ContractState',
default: {},
});
var contractStateInitialized = {};
var contractStateLoading = {};
export function useContractState(key, fetchState, initialState, initializer) {
const [state, setState] = useRecoilState(contractState(key));
const [componentDidMount, setComponentMounting] = useState(false);
const { library } = useWeb3React();
const provider = library?.provider;
const updateState = () => {
fetchState()
.then(setState)
.then(() => {
contractStateInitialized[key] = true;
contractStateLoading[key] = false;
});
};
useEffect(() => {
// ensures that this will only be called once per init or per provider update
// doesn't re-trigger when a component re-mounts
if (provider != undefined && !contractStateLoading[key] && (componentDidMount || !contractStateInitialized[key])) {
console.log('running expensive fetch:', key);
contractStateLoading[key] = true;
if (initializer != undefined) initializer();
updateState();
setComponentMounting(true);
}
}, [provider]);
if (!contractStateInitialized[key] && initialState != undefined) return [initialState, updateState];
return [state, updateState];
}
useSerumContext.js
import { useSerumContract } from '../lib/ContractConnector';
import { useContractState } from './useContractState';
export function useSerumContext() {
const { contract } = useSerumContract();
const fetchState = async () => ({
owner: await contract.owner(),
claimActive: await contract.claimActive(),
});
return useContractState('serumContext', fetchState);
}
The reason why I have so many extra checks is that I don't want to re-fetch the state when the component re-mounts, but the state has already been initialised. The state should however subscribe to updates on provider changes and re-fetch if it has changed.
I am trying to curb useEffect related to nested components. Here are the components:
Parent (it receives data from API):
const ListOfLots = (props) => {
const initial = {listLots: props.lots, form: props.form}
const [lots, setLots] = useState(initial);
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
});
return (
<div>
{
lots.listLots.map(function(lot) {
return <Lot key={lot.uuid} lot={lot} procForm={lots.form}/>
})
}
</div>
)
}
Nested:
const Lot = (props) => {
const initial = {currLot: props.lot, form: props.form};
const [lot, setLot] = useState(initial);
let getWinningBid = (offers) => {
for (let i = 0; i < offers.length; i++) {
console.log("BOOM!!!");
if (offers[i].isTrue === true) {
return offers[i].pip;
}
}
}
return (
...
)
}
While I am using no dependencies at parent's useEffect, I have got an infinite invoking of console.log("BOOM!!!"), that is, of course, unacceptable, but my Nested component rerendered. When I try to use the following type of dependencies at useEffect: [], [lots.form], [lots.listLots] or [lots.listLots.length] my Nested component is not rerendered: it stays blank. So the result is the following: I have an infinite useEffect loop or not-working(?) useEffect.
Is there any way in this case to handle the useEffect?
Use
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
}, [props.lots, props.form]);
This triggers the callback only if the value of props.lots, props.form is changed else it won't be triggered on every rerender as in case of no second argument.
A similar question here might help you find better explanations.
If someone can help me with this ?
I need this function to store the filteredObject key in the state. but when I call this function in componentDidMount(), it didn't work and when I called it in ComponentDidUpdate() it works but going on a infinite loop?
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => { return agent.email === returnedEmail })
if (filteredEmail) {
this.setState({
agentApplicationId: filteredEmail.properties
})
}
}
}
You need to be very careful when setting state in componentDidUpdate. Calling setState updates the component, which triggers componentDidUpdate, which calls setState, and so on, causing the infinite loop. From the React docs:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition...or you’ll cause an infinite loop.
The solution is to add some kind of condition so you're not updating state unnecessarily. For example:
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents, agentApplicationId } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => agent.email === returnedEmail);
// Add an extra condition here to prevent state from updating if the values are already equal.
if (filteredEmail && filteredEmail.properties !== agentApplicationId) {
this.setState({
agentApplicationId: filteredEmail.properties
});
}
}
}
I've been loving getting into hooks and dealing with all the new fun issues that come up with real-world problems :) Here's one I've run into a couple of times and would love to see how you "should" solve it!
Overview: I have created a custom hook to capsulate some of the business logic of my app and to store some of my state. I use that custom hook inside a component and fire off an event on load.
The issue is: my hook's loadItems function requires access to my items to grab the ID of the last item. Adding items to my dependency array causes an infinite loop. Here's a (simplified) example:
Simple ItemList Component
//
// Simple functional component
//
import React, { useEffect } from 'react'
import useItems from '/path/to/custom/hooks/useItems'
const ItemList = () => {
const { items, loadItems } = useItems()
// On load, use our custom hook to fire off an API call
// NOTE: This is where the problem lies. Since in our hook (below)
// we rely on `items` to set some params for our API, when items changes
// `loadItems` will also change, firing off this `useEffect` call again.. and again :)
useEffect(() => {
loadItems()
}, [loadItems])
return (
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
)
}
export default ItemList
Custom useItems Hook
//
// Simple custom hook
//
import { useState, useCallback } from 'react'
const useItems = () => {
const [items, setItems] = useState([])
// NOTE: Part two of where the problem comes into play. Since I'm using `items`
// to grab the last item's id, I need to supply that as a dependency to the `loadItems`
// call per linting (and React docs) instructions. But of course, I'm setting items in
// this... so every time this is run it will also update.
const loadItems = useCallback(() => {
// Grab our last item
const lastItem = items[items.length - 1]
// Supply that item's id to our API so we can paginate
const params = {
itemsAfter: lastItem ? lastItem.id : nil
}
// Now hit our API and update our items
return Api.fetchItems(params).then(response => setItems(response.data))
}, [items])
return { items, loadItems }
}
export default useItems
The comments inside the code should point out the problem, but the only solution I can come up with right now to make linters happy is to supply params TO the loadItems call (ex. loadItems({ itemsAfter: ... })) which, since the data is already in this custom hook, I am really hoping to not have to do everywhere I use the loadItems function.
Any help is greatly appreciated!
Mike
If you plan to run an effect just once, omit all dependencies:
useEffect(() => {
loadItems();
}, []);
You could try with useReducer, pass the dispatch as loadItems as it never changes reference. The reducer only cares if the action is NONE because that is what the cleanup function of useEffect does to clean up.
If action is not NONE then state will be set to last item of items, that will trigger useEffect to fetch using your api and when that resolves it'll use setItems to set the items.
const NONE = {};
const useItems = () => {
const [items, setItems] = useState([]);
const [lastItem, dispatch] = useReducer(
(state, action) => {
return action === NONE
? NONE
: items[items.length - 1];
},
NONE
);
useEffect(() => {
//initial useEffect or after cleanup, do nothing
if (lastItem === NONE) {
return;
}
const params = {
itemsAfter: lastItem ? lastItem.id : Nil,
};
// Now hit our API and update our items
Api.fetchItems(params).then(response =>
setItems(response)
);
return () => dispatch(NONE); //clean up
}, [lastItem]);
//return dispatch as load items, it'll set lastItem and trigger
// the useEffect
return { items, loadItems: dispatch };
};