useDarkMode hook called multiple times onClick - reactjs

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

Related

React prop function called after useState set hook kills hook

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.

State is not updating immediately after onClick in react js

I am clicking on checkbox. When I am checking the checkbox I am trying to update state but it is not updating immediately. I have created an onchange event named handleAllCheckbox.
Here is my code -
const [hello, setHello] = useState(false);
const handleAllCheckbox = (e) => {
setHello(!hello);
if (!hello) {
contactList.map((item) => {
Ids.push(item._id)
});
setTheArray(...theArray, Ids);
}
else {
const newIds = []
setTheArray(...theArray, newIds);
}
}
I want if hello is true then theArray must have Ids & if it is false then theArray should be empty.
You should be using useEffect if you want to handle any side effect, by side effect it means do something when a particular state change. So in your case probably can refer to below
import { useState, useEffect } from 'react';
const [hello, setHello] = useState(false);
useEffect(() => {
if (!hello) {
contactList.map((item) => {
Ids.push(item._id)
});
setTheArray(...theArray, Ids);
}
else {
const newIds = []
setTheArray(...theArray, newIds);
}
}, [hello]);
const handleAllCheckbox = () => setHello(!hello);
Notice [hello] as a second parameter of useEffect, it means do something whenever "hello" state is updated. So now your code is much cleaner as well, handleAllCheckbox only care about handling state hello, and whenever the value of hello being updated, useEffect takes care of that

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

React hook missing dependency

I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])

Using React Hooks, why are my event handlers firing with the incorrect state?

I'm trying to create a copy of this spinning div example using react hooks. https://codesandbox.io/s/XDjY28XoV
Here's my code so far
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [box, setBox] = useState(null);
const [isActive, setIsActive] = useState(false);
const [angle, setAngle] = useState(0);
const [startAngle, setStartAngle] = useState(0);
const [currentAngle, setCurrentAngle] = useState(0);
const [boxCenterPoint, setBoxCenterPoint] = useState({});
const setBoxCallback = useCallback(node => {
if (node !== null) {
setBox(node)
}
}, [])
// to avoid unwanted behaviour, deselect all text
const deselectAll = () => {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
// method to get the positionof the pointer event relative to the center of the box
const getPositionFromCenter = e => {
const fromBoxCenter = {
x: e.clientX - boxCenterPoint.x,
y: -(e.clientY - boxCenterPoint.y)
};
return fromBoxCenter;
}
const mouseDownHandler = e => {
e.stopPropagation();
const fromBoxCenter = getPositionFromCenter(e);
const newStartAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
setStartAngle(newStartAngle);
setIsActive(true);
}
const mouseUpHandler = e => {
deselectAll();
e.stopPropagation();
if (isActive) {
const newCurrentAngle = currentAngle + (angle - startAngle);
setIsActive(false);
setCurrentAngle(newCurrentAngle);
}
}
const mouseMoveHandler = e => {
if (isActive) {
const fromBoxCenter = getPositionFromCenter(e);
const newAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
box.style.transform =
"rotate(" +
(currentAngle + (newAngle - (startAngle ? startAngle : 0))) +
"deg)";
setAngle(newAngle)
}
}
useEffect(() => {
if (box) {
const boxPosition = box.getBoundingClientRect();
// get the current center point
const boxCenterX = boxPosition.left + boxPosition.width / 2;
const boxCenterY = boxPosition.top + boxPosition.height / 2;
// update the state
setBoxCenterPoint({ x: boxCenterX, y: boxCenterY });
}
// in case the event ends outside the box
window.onmouseup = mouseUpHandler;
window.onmousemove = mouseMoveHandler;
}, [ box ])
return (
<div className="box-container">
<div
className="box"
onMouseDown={mouseDownHandler}
onMouseUp={mouseUpHandler}
ref={setBoxCallback}
>
Rotate
</div>
</div>
);
}
export default App;
Currently mouseMoveHandler is called with a state of isActive = false even though the state is actually true. How can I get this event handler to fire with the correct state?
Also, the console is logging the warning:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
Why do I have to include component methods in the useEffect dependency array? I've never had to do this for other simpler component using React Hooks.
Thank you
The Problem
Why is isActive false?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(Note for convenience I'm only talking about mouseMoveHandler, but everything here applies to mouseUpHandler as well)
When the above code runs, a function instance is created, which pulls in the isActive variable via function closure. That variable is a constant, so if isActive is false when the function is defined, then it's always going to be false as long that function instance exists.
useEffect also takes a function, and that function has a constant reference to your moveMouseHandler function instance - so as long as that useEffect callback exists, it references a copy of moveMouseHandler where isActive is false.
When isActive changes, the component rerenders, and a new instance of moveMouseHandler will be created in which isActive is true. However, useEffect only reruns its function if the dependencies have changed - in this case, the dependencies ([box]) have not changed, so the useEffect does not re-run and the version of moveMouseHandler where isActive is false is still attached to the window, regardless of the current state.
This is why the "exhaustive-deps" hook is warning you about useEffect - some of its dependencies can change, without causing the hook to rerun and update those dependencies.
Fixing it
Since the hook indirectly depends on isActive, you could fix this by adding isActive to the deps array for useEffect:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
However, this isn't very clean: if you change mouseMoveHandler so that it depends on more state, you'll have the same bug, unless you remember to come and add it to the deps array as well. (Also the linter won't like this)
The useEffect function indirectly depends on isActive because it directly depends on mouseMoveHandler; so instead you can add that to the dependencies:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
With this change, the useEffect will re-run with new versions of mouseMoveHandler which means it'll respect isActive. However it's going to run too often - it'll run every time mouseMoveHandler becomes a new function instance... which is every single render, since a new function is created every render.
We don't really need to create a new function every render, only when isActive has changed: React provides the useCallback hook for that use-case. You can define your mouseMoveHandler as
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
and now a new function instance is only created when isActive changes, which will then trigger useEffect to run at the appropriate moment, and you can change the definition of mouseMoveHandler (e.g. adding more state) without breaking your useEffect hook.
This likely still introduces a problem with your useEffect hook: it's going to rerun every time isActive changes, which means it'll set the box center point every time isActive changes, which is probably unwanted. You should split your effect into two separate effects to avoid this issue:
useEffect(() => {
// update box center
}, [box])
useEffect(() => {
// expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
End Result
Ultimately your code should look like this:
const mouseMoveHandler = useCallback(e => {
/* ... */
}, [isActive]);
const mouseUpHandler = useCallback(e => {
/* ... */
}, [isActive]);
useEffect(() => {
/* update box center */
}, [box]);
useEffect(() => {
/* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
More info:
Dan Abramov, one of the React authors goes into a lot more detail in his Complete Guide to useEffect blogpost.
React Hooks useState+useEffect+event gives stale state.
seems you are having similar problems. basic issue is that "it gets its value from the closure where it was defined"
try that Solution 2 "Use a ref". in your scenario
Add below useRef, and useEffect
let refIsActive = useRef(isActive);
useEffect(() => {
refIsActive.current = isActive;
});
then inside mouseMoveHandler , use that ref
const mouseMoveHandler = (e) => {
console.log('isActive',refIsActive.current);
if (refIsActive.current) {
I have created an NPM module to solve it https://www.npmjs.com/package/react-usestateref and it seems that it may help and answer your question how to fire the current state.
It's a combination of useState and useRef, and let you get the last value like a ref
Example of use:
const [isActive, setIsActive,isActiveRef] = useStateRef(false);
console.log(isActiveRef.current)
More info:
https://www.npmjs.com/package/react-usestateref

Resources