So I have this bit of code that does not work as expected. Current focus has been set using useState() on the parent component, hence its a state variable. However, when the currentFocus value changes in the parent, focus variable here itself is not updated. I would have expected the re render of the parent component, which in turn rerenders this component would cause the foucs value to change.
import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
const {
name,
cost,
value,
numOwned,
onMouseClick,
cookieId,
currentFocus,
} = props;
let cookieInfoRef = useRef(null);
//My focus doesnt change even if I change currentFocus in parent component
const [focus, setFocus] = useState(currentFocus);
useEffect(() => {
console.log('currentFocus', currentFocus);
console.log('focus', focus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === focus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
}, [focus]);
return (
<React.Fragment>
<button
onClick={onMouseClick}
ref={cookieId === focus ? cookieInfoRef : null}
>
<h3>{name}</h3>
<p>Cost:{cost}</p>
<p>Value:{value}</p>
<p>Owned:{numOwned}</p>
</button>
</React.Fragment>
);
};
export default CookieDetails;
Now I can solve this problem by doing the following instead,
import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
const {
name,
cost,
value,
numOwned,
onMouseClick,
cookieId,
currentFocus,
} = props;
let cookieInfoRef = useRef(null);
useEffect(() => {
console.log('currentFocus', currentFocus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === currentFocus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
});
return (
<React.Fragment>
<button
onClick={onMouseClick}
ref={cookieId === currentFocus ? cookieInfoRef : null}
>
<h3>{name}</h3>
<p>Cost:{cost}</p>
<p>Value:{value}</p>
<p>Owned:{numOwned}</p>
</button>
</React.Fragment>
);
};
export default CookieDetails;
But I only wanted to run the useEffect hook when focus/currentFocus is updated and not after every render. So why does this happen? What am I am missing to understand here.
Also I previously noticed if you did something like this, the handleStuff function always uses the initial value of 100 (so its always 100 + 10) instead of incrementing by 10 each time a key is pressed. I can solve this by removing the empty [] as the second argument in useEffect. However, I expected the handleStuff to still update with the latest value instead, given that the eventListner has already been added on initial render and on second keydown it should add 10 to 110 instead of 100, but it keeps using the initial state value of 100 instead. Why must you clear the old listener and add the new one each time for it work?
[value, setValue] = useState(100)
handleStuff = (event) => {
setValue(value+10)
}
useEffect(() => {
window.addEventListener('keydown', handleStuff)
return () => {
window.removeEventListener('keydown', handleStuff)
}
},[]);
I am not really insterested in the solutions, I am really insterested in understanding how useEffect() and useState() hooks function in this conditions.
Hey the useEffect hook can be a bit confusing.
But the way to think of it is
useEffect(() => {
// Called in an infinite loop (most cases you won't need this)
})
useEffect(() => {
// Called on first render
}, [])
useEffect(() => {
// Called when x y & z updates
}, [x, y, z])
The problem is you aren't listening for the currentFocus update in your code.
useEffect(() => {
// Called when focus updates
}, [focus]);
useEffect(() => {
// Called when props.currentFocus & focus updates
}, [props.currentFocus, focus]);
Now to explain your confusion:
// My focus doesnt change even if I change currentFocus in parent component
const [focus, setFocus] = useState(currentFocus);
When you are passing in currentFocus here the only time the currentFocus value is being passed to the focus useState is on the first render. So you need to listen for the currentFocus update and then set the new focus which should work how you want!
// Focus set to props.currentFocus on first render
const [focus, setFocus] = useState(currentFocus);
useEffect(() => {
// Update focus
setFocus(props.currentFocus)
}, [props.currentFocus])
useEffect(() => {
// currentFocus will be the same as focus now
console.log('currentFocus', currentFocus);
console.log('focus', focus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === focus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
}, [focus]);
Hope that helps!
Related
I'm trying to 'move' in my 10x10 grid by updating the activeCellId state. However none of the methods I tried works. This is my code.
const GridCells: React.FC = () => {
const gridArray = [...Array(100).keys()];
const color = [
"bg-slate-50",
"bg-slate-100",
"bg-slate-200",
"bg-slate-300",
"bg-slate-400",
"bg-slate-500",
"bg-slate-600",
"bg-slate-700",
"bg-slate-800",
"bg-slate-900",
];
const [activeCellId, setActiveCellId] = useState(42);
// useEffect(() => {
// document.addEventListener("keydown", updateActiveCellId, false);
// }, []); // this doesn't work. the activeCellId is only incremented once, and afterwards the setActiveCellId doesn't get called at all
const updateActiveCellId = (e: React.KeyboardEvent) => {
// will eventually be a switch case logic here, for handling arrow up, left, right down
console.log(activeCellId);
setActiveCellId(activeCellId + 1);
};
return (
<div
className="grid-rows-10 grid grid-cols-10 gap-0.5"
// onKeyDown={updateActiveCellId} this also doesn't work
>
{gridArray.map((value, id) => {
const colorId = Math.floor(id / 10);
return (
<div
key={id}
className={
"h-10 w-10 "
+ color[colorId]
+ (id === activeCellId ? " scale-125 bg-yellow-400" : "")
}
>
{id}
</div>
);
})}
</div>
);
};
I'm trying to update a state in the react component by pressing certain keys. I've tried UseEffect with [] dep array and tried onKeyDown and it also doesn't work. I also tried following this useRef way it doesn't work too.
const innerRef = useRef(null);
useEffect(() => {
const div = innerRef.current;
div.addEventListener("keydown", updateActiveCellId, false);
}, []); // this doesn't work at all
const updateActiveCellId = (e: React.KeyboardEvent) => {
console.log(activeCellId);
setActiveCellId(activeCellId + 1);
};
return (
<div
className="grid-rows-10 grid grid-cols-10 gap-0.5"
ref={innerRef}
>
...
)
Try this:
useEffect(() => {
document.addEventListener("keydown", updateActiveCellId, false);
return () => {
document.removeEventListener("keydown", updateActiveCellId, false);
}
}, [activeCellId]);
The [activeCellId] is the dependency of useEffect. Everytimes activeCellId changes, the function inside useEffect will run.
You had an empty dependency, so it ran on initial component mount only.
The returned function containing removeEventListner is executed when the component unmounts (See cleanup function in the docs). That is to ensure you have only one event listener runnign at once.
Documentation
[Problem]
I have a HTML Input element to focus on inside my bottomsheet which is hidden by default. I would like to focus on it when bottomsheet is shown, but I am keep missing it.
[What I've tried]
I already tried autoFocus={true} but it didn't work.
I tried the following, still not working.
const bottomSheetPage = (props) => {
const [bottomSheetOn, setBottomSheetOn] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
~~~ some codes ~~~
useEffect ( () => {
if(props.autoFocus) {
inputRef?.current?.focus()
}
}, [isBottomsheetOn])
~~~ some codes ~~~
<input ref={inputRef}/>
bottomSheetOn is state that controls the toggle of bottomsheet and checked that prop.autoFocus === true.
How can I focus on the element inside bottomsheet when it's shown?
This could have one of two reasons:
props.autoFocus is false
props.autoFocus is true, but useEffect is only called when isBottomsheetOn changes
Try adding props.autoFocus to the list of useEffect dependencies and console.log/debugger inside useEffect to make sure it is called correctly.
useEffect ( () => {
if(props.autoFocus) {
inputRef?.current?.focus()
}
}, [isBottomsheetOn, props.autoFocus]) // will be triggered on props.autoFocus change
If that doesn't help, try to set the focus manually to make sure it's not a problem with the input ref.
import { useState, useEffect, useRef } from 'react'
export default function Component(props) {
const inputRef = useRef(null)
const [on, setOn] = useState(true)
useEffect ( () => {
if (on) {
inputRef?.current?.focus()
}
}, [on])
return (
<>
<input ref={inputRef}/>
<button onClick={() => setOn(prev => !prev)}>Focus</button>
</>
)
}
I found several ways to fix this up, and there was two methods I've actually tried. Either with different advantages.
using IntersectionObserver and setTimeout
you can check if one element intesects other by IntersectionObserver.observe() so I made a recrurring function to check intersection, and then set focus when it's intersecting. Codes is as follows.
const [ticker, setTicker] = useState(true)
const [isIntersecting, setIntersecting] = useState(false)
const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting))
useEffect(() => {
if(props.autoFocus) {
if(inputRef?.current) {
if (isIntersecting) {
inputRef.current.focus()
} else {
setTimerout(() => setTicker(!ticker), 500)
}
}
}
return () => {observer.disconnect()}
}, [ticker])
But this method was focusing on element only once. I needed to focus on it everytime it's shown.
using setTimeout
I figured out that problem was there's time needed for rendering toggled bottomsheet, so I simply gave timeout for focus. And it worked out.
useEffect(() => {
if (focusRef?.current) {
setTimeout(setFocus, 1000)
}
})
const setFocus = () => {
if (focusRef?.current){
focusReft.focus()
}
}
Currently I am working on a project where i am using react and redux to build it.
Scenario
There is a toggle button which will toggle between Group Queue and My Queue. Both these will hit a api and load the data(list of tickets) .[i.e. 2 separate entities]
Now there is a search box while will give the result based on the value i enter. I have added the 'Delay functionality' i.e. after 2 seconds onChange event will fire and it will dispatch an action to all reducers.
Problem
Let's say, i search for a value(68) under group Queue, it works fine. Now if i toggle to my queue, SearchBox value should be null.but it shows as 68
As i told, both queue are separate bodies, if i toggle, searchBox's value should disappear, but its not.
Reason
i am storing a state of search box value and timer to set the value on typing after 2 seconds and on every character I type, setTimeOut and clearTimeOut will be invoked to accommodate the delay functionality.
Thus i have a state inside my React SearchBox Component and once i type and stay put for 2 seconds, then it will dispatch the action via useEffect/ComponentDidUpdate.
i decided to have a state inside the component as i did not want to fire an action on every character user types. Only the intended search Value Redux should store.
Things i tried
i tried with getDerivedStateFromProps, but as i need to use SetState and its asynchronous, its not reflecting the current value.
i have added 2 functionality (onChange and OnEnter). With OnEnter, i think it will work as i can pass the redux store's value and action to the input's value and OnEnter attribute. [i have not tried]
But i want to have the delay functionality. Can anyone suggest an alternative ?
Codes
1. SearchComponent
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { filterAction } from "../../actions";
import "./search.css";
const WAIT_INTERVAL = 2000;
const ENTER_KEY = 13;
const Search = (props) => {
console.log("search", props.value);
let [value, setValue] = useState(props.value);
let [timer, setTimer] = useState(0);
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
const handleKeyDown = (event) => {
if (event.keyCode === ENTER_KEY) {
clearTimeout(timer);
props.searchAction("SEARCH_FILTER", value);
}
};
// GETDERIVEDSTATEFROMPROPS
// if (props.value !== value) {
// setValue("");
// // console.log("check2", props.value, value);
// }
return (
<input
className="search-box"
type="search"
placeholder="Search any Request Id/ Email Id..."
aria-label="Search"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
></input>
);
};
const mapStateToProps = (store) => ({
value: store.filterValues.searchFilter,
});
export default connect(mapStateToProps, { filterAction })(Search);
Redux Redcuer (FilterReducer) -- working fine i.e. on toggling, searchValue is getting emptied('').
export const filterReducer = (filterValues = {}, action) => {
switch (action.type) {
case "TOGGLE_QUEUE":
return {
order: [],
tileFilter: action.payload.tileType,
searchFilter: "",
buttonFilter: {},
timelineFilter: "all",
};
case "SEARCH_FILTER":
let arr = [...filterValues.order];
arr.push("SEARCH_FILTER");
let ob = {
...filterValues,
searchFilter: action.payload.value,
order: arr,
};
return ob;
}
return filterValues;
};
Please let me know if you have any solutions.
EDIT 1
i tried with props.value on UseEffect, now the search itself is not working..
useEffect(() => {
// setValue(props.value); // not working
setTimer(
setTimeout(() => {
if (timer) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
setValue(props.value); // not working
return () => clearTimeout(timer);
}, [props.value]);
Edit2
i added a separate useffect as suggested by Sabbit, but if i type 68, the search functionality is getting invoked on 6 and typed value (68) alternatively.
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer && value.length > 0)
props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
useEffect(() => {
if (value !== props.value) setValue(props.value);
}, [props.value]);
Did you tried using another useEffect for props.value?
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer && value.length > 0) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
useEffect(() => {
setValue(props.value);
}, [props.value]);
That will update the value in the state each time the props.value is changed. I believe the searchValue from reducer is passed via props to the Search component.
Update: I have updated the condition to execute props.filterAction("SEARCH_FILTER", value) from if(timer) to if(timer && value.length > 0). We need to do that because everytime the props changes we are setting the value in state and we have used an useEffect depending on the change of value in state. That is why it is searching with empty string. We need to make sure that the searching doesn't happen unless we have something in the input field.
I'm trying to create arrow based keyboard controls for a game I'm working on. Of course I'm trying to stay up to date with React so I wanted to create a function component and use hooks. I've created a JSFiddle for my buggy component.
It's almost working as expected, except when I press a lot of the arrow keys at the same time. Then it seems like some keyup events aren't triggered. It could also be that the 'state' is not updated properly.
Which I do like this:
const ALLOWED_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
const [pressed, setPressed] = React.useState([])
const handleKeyDown = React.useCallback(event => {
const { key } = event
if (ALLOWED_KEYS.includes(key) && !pressed.includes(key)) {
setPressed([...pressed, key])
}
}, [pressed])
const handleKeyUp = React.useCallback(event => {
const { key } = event
setPressed(pressed.filter(k => k !== key))
}, [pressed])
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
})
I have the idea that I'm doing it correctly, but being new to hooks it is very likely that this is where the problem is. Especially since I've re-created the same component as a class based component:
https://jsfiddle.net/vus4nrfe/
And that seems to work fine...
There are 3 key things to do to make it work as expected just like your class component.
As others mentioned for useEffect you need to add an [] as a dependency array which will trigger only once the addEventLister functions.
The second thing which is the main issue is that you are not mutating the pressed array's previous state in functional component as you did in class component, just like below:
// onKeyDown event
this.setState(prevState => ({
pressed: [...prevState.pressed, key],
}))
// onKeyUp event
this.setState(prevState => ({
pressed: prevState.pressed.filter(k => k !== key),
}))
You need to update in functional one as the following:
// onKeyDown event
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
// onKeyUp event
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
The third thing is that the definition of the onKeyDown and onKeyUp events have been moved inside of useEffect so you don't need to use useCallback.
The mentioned things solved the issue on my end. Please find the following working GitHub repository what I've made which works as expected:
https://github.com/norbitrial/react-keydown-useeffect-componentdidmount
Find a working JSFiddle version if you like it better here:
https://jsfiddle.net/0aogqbyp/
The essential part from the repository, fully working component:
const KeyDownFunctional = () => {
const [pressedKeys, setPressedKeys] = useState([]);
useEffect(() => {
const onKeyDown = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key) && !pressedKeys.includes(key)) {
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
}
}
const onKeyUp = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key)) {
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
}
}
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
return () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keyup', onKeyUp);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>
<h3>KeyDown Functional Component</h3>
<h4>Pressed Keys:</h4>
{pressedKeys.map(e => <span key={e} className="key">{e}</span>)}
</>
}
The reason why I'm using // eslint-disable-next-line react-hooks/exhaustive-deps for the useEffect is because I don't want to reattach the events every single time once the pressed or pressedKeys array is changing.
I hope this helps!
User #Vencovsky mentioned the useKeyPress recipe by Gabe Ragland. Implementing this made everything work as expected. The useKeyPress recipe:
// Hook
const useKeyPress = (targetKey) => {
// State for keeping track of whether key is pressed
const [keyPressed, setKeyPressed] = React.useState(false)
// If pressed key is our target key then set to true
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true)
}
}
// If released key is our target key then set to false
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false)
}
}
// Add event listeners
React.useEffect(() => {
window.addEventListener('keydown', downHandler)
window.addEventListener('keyup', upHandler)
// Remove event listeners on cleanup
return () => {
window.removeEventListener('keydown', downHandler)
window.removeEventListener('keyup', upHandler)
}
}, []) // Empty array ensures that effect is only run on mount and unmount
return keyPressed
}
You can then use that "hook" as follows:
const KeyboardControls = () => {
const isUpPressed = useKeyPress('ArrowUp')
const isDownPressed = useKeyPress('ArrowDown')
const isLeftPressed = useKeyPress('ArrowLeft')
const isRightPressed = useKeyPress('ArrowRight')
return (
<div className="keyboard-controls">
<div className={classNames('up-button', isUpPressed && 'pressed')} />
<div className={classNames('down-button', isDownPressed && 'pressed')} />
<div className={classNames('left-button', isLeftPressed && 'pressed')} />
<div className={classNames('right-button', isRightPressed && 'pressed')} />
</div>
)
}
Complete fiddle can be found here.
The difference with my code is that it use hooks and state per key instead of all the keys at once. I'm not sure why that would matter though. Would be great if somebody could explain that.
Thanks to everyone who tried to help and made the hooks concept clearer for me. And thanks for #Vencovsky for pointing me to the usehooks.com website by Gabe Ragland.
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [handleKeyDown, handleKeyUp]); // <---- Add this deps array
You need to add the handlers as dependencies to the useEffect, otherwise it gets called on every render.
Also, make sure your deps array is not empty [], because your handlers could change based on the value of pressed.
All the solutions I found were pretty bad. For instance, the solutions in this thread only allow you to hold down 2 buttons, or they simply don't work like a lot of the use-hooks libraries.
After working on this for a long time with #asafaviv from #Reactiflux I think this is my favorite solution:
import { useState, useLayoutEffect } from 'react'
const specialKeys = [
`Shift`,
`CapsLock`,
`Meta`,
`Control`,
`Alt`,
`Tab`,
`Backspace`,
`Escape`,
]
const useKeys = () => {
if (typeof window === `undefined`) return [] // Bail on SSR
const [keys, setKeys] = useState([])
useLayoutEffect(() => {
const downHandler = ({ key, shiftKey, repeat }) => {
if (repeat) return // Bail if they're holding down a key
setKeys(prevKeys => {
return [...prevKeys, { key, shiftKey }]
})
}
const upHandler = ({ key, shiftKey }) => {
setKeys(prevKeys => {
return prevKeys.filter(k => {
if (specialKeys.includes(key))
return false // Special keys being held down/let go of in certain orders would cause keys to get stuck in state
return JSON.stringify(k) !== JSON.stringify({ key, shiftKey }) // JS Objects are unique even if they have the same contents, this forces them to actually compare based on their contents
})
})
}
window.addEventListener(`keydown`, downHandler)
window.addEventListener(`keyup`, upHandler)
return () => {
// Cleanup our window listeners if the component goes away
window.removeEventListener(`keydown`, downHandler)
window.removeEventListener(`keyup`, upHandler)
}
}, [])
return keys.map(x => x.key) // return a clean array of characters (including special characters š)
}
export default useKeys
I believe you're Breaking the Rules of Hooks:
Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
You're calling the setPressed hook inside a function passed to useCallback, which basically uses useMemo under the hood.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
https://reactjs.org/docs/hooks-reference.html#usecallback
See if removing the useCallback in favor of a plain arrow function solves your problem.
useEffect runs on every render, resulting on adding/removing your listeners on each keypress. This could potential lead to a key press/release without a listener attached.
Suppling an empty array [] as second parameter to useEffect, React will know that this effect does not depend on any of the props/state values so it never needs to re-run, attaching and cleaning up your listeners once
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
I have a event when you resize the window will show desktop sidebar or mobile sidebar. if window is less than But there are variables that aren't updated immediately to show sidebar if I'm in desktop window, I could that with class
I've created a sandbox https://codesandbox.io/s/sidebar-hooks-8oefi
to see the code, I have the class component in App_class.js which if I replace in App it works, but with hooks (App_hooks.js file, by default in App.js) I can't make it works
Thanks for your attention. Iām looking forward to your reply.
with class I could do that using:
if (isMobile !== wasMobile) {
this.setState({
isOpen: !isMobile
});
}
const App = props => {
//minicomponent to update width
const useListenResize = () => {
const [isOpen, setOpen] = useState(false);
const [isMobile, setMobile] = useState(true);
//const [previousWidth, setPreviousWidth] = useState( -1 );
let previousWidth = -1;
const updateWidth = () => {
const width = window.innerWidth;
const widthLimit = 576;
let newValueMobile = width <= widthLimit;
setMobile(isMobile => newValueMobile);
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
//setPreviousWidth( width );
previousWidth = width;
};
useEffect(() => {
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
window.removeEventListener("resize", updateWidth);
};
// eslint-disable-next-line
}, []);
return isOpen;
};
const [isOpen, setOpen] = useState(useListenResize());
const toggle = () => {
setOpen(!isOpen);
};
return (
<div className="App wrapper">
<SideBar toggle={toggle} isOpen={isOpen} />
<Container fluid className={classNames("content", { "is-open": isOpen })}>
<Dashboard toggle={toggle} isOpen={isOpen} />
</Container>
</div>
);
};
export default App;
with setState didn't work
The issue is after setting isMobile value here,
setMobile(isMobile => newValueMobile);
You are immediately refering that value,
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
Due to async nature of setState, your are getting previous value of isMobile here all the times.
To make this work you need to make some change to your code.
You are directly mutating previousWidth value, you should have previousWidth in a state and use setter function to change the value.
const [previousWidth, setPreviousWidth] = useState(-1);
setPreviousWidth(width); //setter function
You cannot get value immediately after setState. You should use another useEffect with isMobile and previousWidth as dependency array.
useEffect(() => {
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
}, [isMobile, previousWidth]);
Demo
Since updateWidth is registered once on component mount with useEffect, then it will refer to a stale state (isMobile), which is always true, therefore, setOpen(isOpen => !isMobile) will never work.
EDIT: The following code illustrates a simpler version of your issue:
const Counter = () => {
const [count, setCount] = useState(0)
const logCount = () => {
console.log(count);
}
useEffect(() => {
window.addEventListener('resize', logCount);
}, [])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
If you run this code, you will notice that even if you change the value of count by clicking on the button, you will always get the initial value when resizing the browser (check the console), and that's because logCount refer to a state that was obtained at the moment it was defined.
#ravibagul91 answer works, because it access the state from useEffect, and not from the event handler.
You may wanna check: React hooks behaviour with event listener and Why am I seeing stale props or state inside my function?, it will give you a better idea of what's going on.