React prop function called after useState set hook kills hook - reactjs

I have an PostImagesList component that updates when an image is removed. I also send the removed image id to the parent for deletion if the post update is ultimately committed.
I'm trying to understand why a prop function (onRemoveImage) kills the setImages useState hook, unless I remove the same image twice(?) Presently in the parent I am only logging the id sent from the child so nothing is changing with the data.
// <PostImagesList />
const handleRemoveImage = (e, idx, imageId) => {
e.stopPropagation();
let newFilesArr = [...images];
newFilesArr.splice(idx, 1);
setImages(newFilesArr);
onRemoveImage(imageId); // If I comment this out, the setImages updates correctly
};
images is initially set in PostImagesList in a useEffect hook:
useEffect(() => {
setImages(
imagesByPostId?.edges
? imagesByPostId?.edges
.filter(Boolean)
.map((edge) => edge.node)
.filter(Boolean)
: [],
);
};
I also tried setting the images in the declaration instead of in a useEffect hook:
const [images, setImages] = useState(() => {
return imagesByPostId?.edges
? imagesByPostId?.edges
.filter(Boolean)
.map((edge) => edge.node)
.filter(Boolean)
: [];
});
I want to update the list locally in <PostImagesList/>, and send the deleted image id to the parent, but not rerender <PostImagesList/>.

I can't see where you declared setImages but if you want to set the initial state you can do like:
const [state, setState] = React.useState(whatever)
If you want to set it depending on complex conditions, then you can use React.useReducer instead of React.useState or define initial state like:
const [state, setState] = React.useState(() => {
if (itsCool) return "cool";
else return "not cool";
});
If you need to use that useEffect hook and you want to set something at the initial render then you need to pass an empty dependency Array, else you can run into "Maximum update depth exceeded" because setState will call repeatedly. So do it like:
useEffect(() => {
setImages(
imagesByPostId?.edges
? imagesByPostId?.edges
.filter(Boolean)
.map((edge) => edge.node)
.filter(Boolean)
: [],
);
}, []); // here is the empty dependency array
More about useEffect if you are interested:
Note that useEffect is not only componentDidMount but also componentDidUpdate, so the callback function you pass will executed on every component-update. If you pass a dependency array the callback function you pass will get executed only when the dependeny (reference) changes. Something like:
const { useEffect } = (function() {
var _dep = null;
function useEffect(clb, dep) {
if (_dep === null) {
_dep = dep;
clb();
} else if (_dep !== dep) {
_dep = dep;
clb();
} else {
return;
}
}
return {
useEffect
};
})();
var counter = 1;
const cbk = () => console.log(counter);
useEffect(cbk, counter);
useEffect(cbk, counter); // cbk will not execute
useEffect(cbk, counter); // cbk will not execute
counter++;
useEffect(cbk, counter);
to simpily the useEffect funtion we don't pass a dependency array we just pass one dependency.

Related

How to checddsa hooks?

I need to detect every change of variable state BUT check code:
useEffect(() => {
if (value) {
setSelect(value);
}
}, [value]);
code above checkin every second and i got error:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
I need to check only when value is changed not every second!
value is props data from parent component.
const OrganisationUnitInput = ({
input
}) => {
const [showModal, setShowModal] = useState(false);
const value = input.value || [];
const value = input.value || [];
const handleChange = useCallback(
(value) => {
input.onChange(value);
setShowModal(false);
},
[input]
);
}
It is happening because you are setting value state and passing same value as dependency array . which results in re rendering
You misunderstood the useEffect hook, when you pass the second argument with some variable you are telling to react that the function of the effect must be executed if the variable change.
So in your code You are making an infinity loop.
To detect the change on the var only pass the var in the array and your function will be executed if any change occurred.
const Comp = () => {
const [value, setValue] = useState(0)
useEffect(() => {
// This code will be execute any time that the var value
// change
console.log("The value change, now is: ", value)
// so if you change the var here your will cause another call
// to this effect.
}, [value])
return <button onClick={() => { setValue(prev => prev + 1) }}>
Change Value
</button>
}

I am trying to use callback in hook but can not get latest context value in the callback

const Demo = () => {
const { name } = useContext(AppContext);
function emiterCallback(val) {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, []);
}
in class component
this.emiterCallback = this.emiterCallback.bind(this) can solve my question, but how to use it in hook ?
The problem you have here is due to the fact that useEffect with an empty array dependency only runs once - when the component mounts. This means that the emiterCallback it assigns as the event function is the very first one that's made on the first render. Since you just declare emiterCallback in the body of the function, it gets remade every single re-render, so after a single re-render, it will be a different one to the event one you assigned when the component mounted. Try something like this:
import React, { useCallback, useContext, useEffect } from 'react';
...
const Demo = () => {
const { name } = useContext(AppContext);
// Assign it to a memoized function that will recalculate as needed when the context value changes
const emiterCallback = useCallback((val) => {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}, [name]);
// Adding the function as a dependency means the .on function should be updated as needed
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, [emiterCallback]);
}
This code isn't tested but you get the idea
use useCallback to memorize the effect no need for bind since there is no this as it is not a class,
Here read more about it -
How can I bind function with hooks in React?

useDarkMode hook called multiple times onClick

I'm trying to build an SSR compatible (flicker-free) dark mode using a custom hook. I want to call it from multiple components which should stay in sync by using an event bus (i.e. emitting custom events and registering corresponding listeners in useEffect).
The problem I'm having is that every time I trigger onClick={() => setColorMode(nextMode)}, it's called multiple times. In the screenshot below, only the first of the nine lines that appear inside the red box when clicking DarkToggle is expected. (The logs above the red box occur during initial page load.)
What's causing these extra calls and how can I avoid them?
An MVP of what I'm trying to build is on GitHub. Here's what the hooks look like:
useDarkMode
import { useEffect } from 'react'
import {
COLORS,
COLOR_MODE_KEY,
INITIAL_COLOR_MODE_CSS_PROP,
} from '../constants'
import { useLocalStorage } from './useLocalStorage'
export const useDarkMode = () => {
const [colorMode, rawSetColorMode] = useLocalStorage()
// Place useDarkMode initialization in useEffect to exclude it from SSR.
// The code inside will run on the client after React rehydration.
// Because colors matter a lot for the initial page view, we're not
// setting them here but in gatsby-ssr. That way it happens before
// the React component tree mounts.
useEffect(() => {
const initialColorMode = document.body.style.getPropertyValue(
INITIAL_COLOR_MODE_CSS_PROP
)
rawSetColorMode(initialColorMode)
}, [rawSetColorMode])
function setColorMode(newValue) {
localStorage.setItem(COLOR_MODE_KEY, newValue)
rawSetColorMode(newValue)
if (newValue === `osPref`) {
const mql = window.matchMedia(`(prefers-color-scheme: dark)`)
const prefersDarkFromMQ = mql.matches
newValue = prefersDarkFromMQ ? `dark` : `light`
}
for (const [name, colorByTheme] of Object.entries(COLORS))
document.body.style.setProperty(`--color-${name}`, colorByTheme[newValue])
}
return [colorMode, setColorMode]
}
useLocalStorage
import { useEffect, useState } from 'react'
export const useLocalStorage = (key, initialValue, options = {}) => {
const { deleteKeyIfValueIs = null } = options
const [value, setValue] = useState(initialValue)
// Register global event listener on initial state creation. This
// allows us to react to change events emitted by setValue below.
// That way we can keep value in sync between multiple call
// sites to useLocalStorage with the same key. Whenever the value of
// key in localStorage is changed anywhere in the application, all
// storedValues with that key will reflect the change.
useEffect(() => {
let value = localStorage[key]
// If a value isn't already present in local storage, set it to the
// provided initial value.
if (value === undefined) {
value = initialValue
if (typeof newValue !== `string`)
localStorage[key] = JSON.stringify(value)
localStorage[key] = value
}
// If value came from local storage it might need parsing.
try {
value = JSON.parse(value)
// eslint-disable-next-line no-empty
} catch (error) {}
setValue(value)
// The CustomEvent triggered by a call to useLocalStorage somewhere
// else in the app carries the new value as the event.detail.
const cb = (event) => setValue(event.detail)
document.addEventListener(`localStorage:${key}Change`, cb)
return () => document.removeEventListener(`localStorage:${key}Change`, cb)
}, [initialValue, key])
const setStoredValue = (newValue) => {
if (newValue === value) return
// Conform to useState API by allowing newValue to be a function
// which takes the current value.
if (newValue instanceof Function) newValue = newValue(value)
const event = new CustomEvent(`localStorage:${key}Change`, {
detail: newValue,
})
document.dispatchEvent(event)
setValue(newValue)
if (newValue === deleteKeyIfValueIs) delete localStorage[key]
if (typeof newValue === `string`) localStorage[key] = newValue
else localStorage[key] = JSON.stringify(newValue)
}
return [value, setStoredValue]
}
You have the below useEffect
useEffect(() => {
const initialColorMode = document.body.style.getPropertyValue(
INITIAL_COLOR_MODE_CSS_PROP
)
rawSetColorMode(initialColorMode)
}, [rawSetColorMode])
Since this useEffect has a dependency on rawSetColorMode, the useEffect runs whenever rawSetColorMode changes.
Now rawSetColorMode internally calls setValue until somecondition inside rawSetColorMode results in setValue to not be called
Now reading by the variable names, it seems you only needed to all the above useEffect on initial render and hence you could simply write it as
useEffect(() => {
const initialColorMode = document.body.style.getPropertyValue(
INITIAL_COLOR_MODE_CSS_PROP
)
rawSetColorMode(initialColorMode)
}, []) // empty dependency to make it run on initial render only
And that should fix your issue
Now you might get a ESLint warning for empty dependency, you can either choose to disable it like
useEffect(() => {
const initialColorMode = document.body.style.getPropertyValue(
INITIAL_COLOR_MODE_CSS_PROP
)
rawSetColorMode(initialColorMode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
or go by the method of memoizing rawSetColorMode method using useCallback so that it is only created once, which might be difficult to do in your case since have multiple dependencies inside of it

Reading component state just after setting when using useState hook in react

This console.log is not working: It'll just print the previous state value as set is async.
const SomeCompo = () => {
const [count, set] = useState(0);
const setFun = () => {
console.log(count);
set(count + 1);
console.log(count);
}
return <button onClick={setFun}>count: {count}</button>
}
I had to read the count in the render itself:
const SomeCompo = () => {
const [count, set] = useState(0);
console.log(count);
const setFun = () => {
set(count + 1);
}
return <button onClick={setFun}>count: {count}</button>
}
Is there a better way to read the value as I don't want to console for every render.
You can use useEffect for this,
useEffect(() => {
console.log(count);
}, [count]) //[count] is a dependency array, useEffect will run only when count changes.
I would suggest not to use setInterval. I would do something like useEffect. This function will be called each time you do a setState. Just like you had callback after setState. Pass the count state in the array, it will watch only for the count change in the state and console your count.
useEffect(() => {
console.log(count);
}, [count]);
Also if you dont need to rerender your other components, you might wanan use useMemo and useCallback. https://www.youtube.com/watch?v=-Ls48dd-vJE
Here to more read: https://reactjs.org/docs/hooks-effect.html
The way to get a state value is to use useEffect and use the state as a dependency. This means that when we change a value the render cycle will finish and a new one will start, then useEffect will trigger:
useEffect( () => { console.log(value); }, [value] );
If you would need to read the value in the same cycle as it is changed a possibility could be to use the useState set function. This shows the latest value just before updating it:
setValue( latest_value => {
const new_value = latest_value + 1;
console.log(new_value);
return new_value;
} );

useEffect lazy created cleanup function

I'm trying to create hook that is is using an effect in which side effect function returns the cleanup callback. However I want to call it only when component is unmounted, not on the rerender.
Normal approach when you call useEffect with empty deps array won't work here as the cleanup function is created only once, on the first call of the hook. But my clean up is created later, so there is no way to change it.
function useListener(data) {
const [response, updateResponse] = useState(null);
useEffect(
() => {
if (data) {
const removeListener = callRequest(data, resp => {
updateResponse(resp);
});
return removeListener;
}
},
[data]
);
return response;
}
This comes down to a following problem: In normal class component, the willComponentUnmount could make a decision based on a current component state but in case of useEffect, state is passed via closure to the cleanup and there is no way to pass the information later if the state has changed
You can use useRef to save and update your callback function
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more
function useListener(data) {
const [response, updateResponse] = useState(null);
const cleanUpCallbackRef = useRef(() => {});
useEffect(
() => {
if (data) {
cleanUpCallbackRef.current = callRequest(data, resp => {
updateResponse(resp);
});
}
},
[data]
);
useEffect(() => {
return () => {
cleanUpCallbackRef.current();
}
}, []);
return response;
}
I create a simple example here

Resources