React useEffect inifinite loop when lifting state and state modifier - javascript

I have a component which lifts its state and the state modifier to the parent:
export default function DamageInput({ data, onChange }){
const [qty, setQty] = useState(data?.qty || null);
const [selectedSize, setSelectedSize] = useState(data?.size || null);
const [selectedSuppl, setSelectedSuppl] = useState(data?.suppl || []);
useEffect(() => {
if(!selectedSize) onChange(null);
else {
const damageObject = {
qty: qty,
size: selectedSize,
suppl: selectedSuppl
}
onChange(damageObject);
}
}, [selectedSize, qty, selectedSuppl, onChange]);
...
The parent which consumes the component:
<DamageInput onChange={(damageObject) => handleDamageChange('bonnet', damageObject)} data={data.damage.bonnet} />
And the handler function:
const handleDamageChange = (panel, damageObject) => {
if(!damageObject){
if(data.damage[panel]){
setData(prevState => {
const damageCopy = {...prevState.damage};
delete damageCopy[panel];
return {...prevState, damage: damageCopy};
});
}
}
else setData(prevState => ({...prevState, damage: {...prevState.damage, [panel]: damageObject}}));
};
The problem is that as soon as the onChange function called with the damageObject parameter, it causes an infinite loop in the first useEffect. I think the problem is that i had to pass the onChange function as a dependency and it changes every time when rerenders because i pass an "anonymus function" with a parameter to the onChange event. Im stuck for hours now and couldnt find the answer. Thank you

Related

context api - useEffect doesn't fire on first render - react native

The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.

Props defined by async function by Parent in UseEffect passed to a child component don't persist during its UseEffect's clean-up

Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.

useState set does not update right away in react

I have one question regarding to my customized useCallBack that I really have no idea how I should fix it. Under my hook useValueChange, I use one local variable prevValue to store the state of input value from hook. the logic is if prevValue object does not match with updateValue object. go to the if/else logic and then update prevValue. The issue I have are
useCallBack block still run even JSON.stringify(updateValue) does not change
setPrevValue does not update prevValue right away. as useState runs async
so wheneve I call the hook multiple time even updateValue does not change, the apicall updateValueApiCall trigger consistently
any idea how I should fix?
function useValueChange(updatedValue) {
const [prevValue, setPrevValue] = useState<ValueQueryVariables>({name: '', address: '', info:{a: '', b''}});
const onValueChange = useCallback(async (): Promise<void> => {
const isValueUpdate = !isEqual(prevValue, updateValue);
if (isValueUpdate) {
console.log({updatedValue, prevValue});
setPrevValue(() => updatedValue);
await updateValueApiCall({...updatedValue});
}
}, [JSON.stringify(updateValue)]);
return {onValueChange};
}
//component Level
const App: FC = (props) => {
const {inputValue} = props
const {onValueChange} = useValueChange(inputValue)
const handleOnClick = useCallBack(() =>{
onValueChange()
})
return (
<div><button onClick={handleOnClick}>click</button></div>
)
}
Update
after some research, thinking to use effect try to make it update. but I still do not get it work.
all suggestions are welcome
function useValueChange(updatedValue) {
const [prevValue, setPrevValue] = useState<ValueQueryVariables>({name: '', address: '', info:{a: '', b''}});
const [isUpdate, setUpdate] = useState(false);
const onValueChange = useCallback(async (): Promise<void> => {
const isValueUpdate = !isEqual(prevValue, updateValue);
setUpdate(isValueUpdate)
if (isValueUpdate) {
console.log({updatedValue, prevValue});
setPrevValue(() => updatedValue);
await updateValueApiCall({...updatedValue});
}
}, [JSON.stringify(updateValue)]);
useEffect(()=>{
if (isUpdate){
setPrevValue(updateValue)
}
}, [JSON.stringify(updateValue), isUpdate, setUpdate])
return {onValueChange};
}

Separate debounce for each React component instance

I'm using a debounce library (tried different ones but currently the one from lodash) in a react component in order to avoid executing code too often while scrolling in the browser.
The problem is that I have multiple instances of the react component and it seems that the debounce function is accidentally shared between those instances. Consequently the function code with '... some code here' is only executed in one instance and not in all instances of the react component. The debounce functionality works great if I have only one instance of my component rendered.
useEffect(() => {
document.querySelector(props.scrollSelector!)?.addEventListener('scroll', e => {
setViewport(props, state, e.target as HTMLDivElement, ref)
}, true)
}, [state.obj])
const setViewport = debounce((p: Props, s: State, rowHeaderObj: any, scrollContainer: HTMLDivElement, ref: any) => {
// ... some code here
}, 20)
Is there some way to change the code so the debounce function works for each instance separately? Please consider that the react component instances have unique keys assigned so that should not be the issue.
One approach could be to create a new debounced function each time you register the event listener instead of reusing the same function, in which case the event handler would be debounced independently within each instance of your component.
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = (props) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
const srollableElement = document.querySelector(props.scrollSelector!)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(props, state, e.target as HTMLDivElement, ref)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, props, ref])
return <></>
}
As a side note, be careful with this usage of useEffect, as (I think) the props parameter that's passed to your component will change each time the parent component re-renders, causing useEffect to potentially re-run very often. One fix for this is making sure the dependencies array passed to useEffect only contains primitive or stable values. Feel free to read this section of the React docs for a discussion of this topic. Taking this into consideration, you might want to re-write the above example as follows (depending on the shape of the Props type):
interface Props {
scrollSelector?: string
b: string
c: number
}
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = ({ scrollSelector, b, c }) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
if (!scrollSelector) {
return
}
const srollableElement = document.querySelector(scrollSelector)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(
{ scrollSelector, b, c },
state,
e.target as HTMLDivElement,
ref
)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, scrollSelector, b, c, ref])
return <></>
}

React Hook useEffect has a missing dependency when passing empty array as second argument

I have the following useEffect hook in my functional component that I only want to use once (when the component mounts) in order to load some data from an API:
const Gear = () => {
const [weaponOptions, setWeaponOptions] = useState([
{
key: "0",
label: "",
value: "null"
}
]);
const [weapon, setWeapon] = useState("null");
useEffect(() => {
console.log("Gear.tsx useEffect");
const fetchWeaponsOptions = async (): Promise<void> => {
const weaponsData = await getWeapons();
const newWeaponOptions: DropdownOptionType[] = [
...weaponOptions,
...weaponsData.map(({ id, name }) => {
return {
key: id,
label: name,
value: id
};
})
];
setWeaponOptions(newWeaponOptions);
};
fetchWeaponsOptions();
}, []);
// TODO add weapon dropdown on change, selected weapon state
const handleWeaponChange = ({ value }: DropdownOptionType): void => {
setWeapon(value);
};
return (
<div>
<h2>Gear:</h2>
<Dropdown
defaultValue={weapon}
label="Weapon"
name="weapon"
options={weaponOptions}
onChange={handleWeaponChange}
/>
</div>
);
};
A React documentation note states that this is valid practice when you only want the effect to run on mount an unmount:
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
But I am getting the following create-react-app linter warning:
35:6 warning React Hook useEffect has a missing dependency: 'weaponOptions'. Either include it or remove the dependency array react-hooks/exhaustive-deps
The useEffect triggers if the weaponOptions array changes if I pass it as a dependency, resulting in an endless loop because the hook itself changes the weaponOptions state. The same thing happens if I omit the empty array argument.
What is the correct approach here?
I only want to use once (when the component mounts) in order to load some data from an API
Therefore according to your logic, you don't need to depend on the component's state:
const INITIAL = [
{
key: '0',
label: '',
value: 'null'
}
];
const App = () => {
const [weaponOptions, setWeaponOptions] = useState(INITIAL);
useEffect(() => {
const fetchWeaponsOptions = async () => {
const weaponsData = await getWeapons();
const weaponsOptions = [
...INITIAL, // No state dependency needed
...weaponsData.map(({ id, name }) => {
return {
key: id,
label: name,
value: id
};
})
];
setWeaponOptions(weaponsOptions);
};
}, []);
return <></>;
};
But, it is common when you want to use a useEffect once, and it depends on a state, to use a boolean reference like so:
const App = () => {
const [weaponOptions, setWeaponOptions] = useState(INITIAL);
const isFirstFetch = useRef(true);
useEffect(() => {
const fetchWeaponsOptions = async () => {...}
if (isFirstFetch.current) {
fetchWeaponsOptions();
isFirstFetch.current = false;
}
}, [weaponOptions]);
return <></>;
};
As you can see that is not an error but a warning. React is telling you that you are using weaponOptions inside useEffect but you didn't pass it as a dependency. Again, that is just a warning, you don't have to do it.

Categories

Resources