I have here a subset of my code – it won’t run as is but outlines what I am doing. Essentially I am placing a floormap image on the floor, checking the size and adjusting for scale of physical size to screen size (getScale()
). Then when scale is updated I load the desks to position on that floorplan, passing in the scale. I am also adding the desk to an array of Refs so I can access the div to adjust the border.
Up until this point, everything works and the border is adjusted when I click the desk. But pressing an arrow key and bringing up the keypress function, none of my state variables is populated.
Why not?
const componponent=()=>{
const mapRef = useRef();
const desksRef = useRef([]);
const [mapScale, setMapScale] = useState();
const [desks, setDesks] = useState({ past: [], present: [], future: [] });
const [currentDesk, setDesk] = useState(-1);
const [currentMap, setCurrentMap] = useState(-1);
const [maps, setMaps] = useState({ maps: [] });
const [editMode, setEditMode] = useState(false);
useEffect(() => {
window.addEventListener('resize', updateSize);
window.addEventListener('keydown', keyDown);
return () => {
window.removeEventListener('resize', updateSize);
window.removeEventListener('keydown', keyDown);
}
}, [])
useEffect(() => {
const desks = buildDesks();
setDesks({ ...desks, present: maps.maps[currentMap]?.desks });
setDeskComponents(desks);
}, [editMode])
useEffect(() => {
const desks = buildDesks();
setDesks({ present: maps.maps[currentMap]?.desks });
setDeskComponents(desks);
}, [mapScale]);
const getScale = () => {
const site = maps.maps[currentMap] //the id on the select
if (!site) return;
let map = mapRef.current;
let rect = map?.getBoundingClientRect();
let mapWidth = rect.width;
let mapHeight = rect.height;
let scaleW = (mapWidth && site?.wMM) ? (mapWidth / site.wMM) : 1;
let scaleH = (mapHeight && site?.hMM) ? (mapHeight / site.hMM) : 1;
setMapScale({ height: scaleH, width: scaleW });
}
const DeskFns = {
Update_Desklist: setDesks,
Update_Desk_Data: updateDesk,
Set_UserImage: setUserImage,
Set_Current: setCurrentDesk,
Get_Current: () => desks.present[currentDesk].id,
Press_Key:keyDown,
//redraw: redraw,
}
const buildDesk = (desk, index) => {
const res =
<Desk key={desk.id}
desk={desk}
isEditing={editMode}
desks={desks.present}
scale={mapScale}
deskTypes={deskTypes}
currentDesk={currentDesk}
fns={DeskFns}
ref={el => desksRef.current[index] = el}
index={index}
onKeyDown={keyDown}
/>
return res;
}
const buildDesks = () => {
//getScale();
try {
let res = maps.maps[currentMap]?.desks.map((desk, index) => buildDesk(desk, index));
//let res = desks.present.map(desk => buildDesk(desk);
return res
} catch (error) {
console.error('Error building desks:', error);
return null;
}
};
function keyDown(e) {
if (currentDesk > -1 && editMode == true) {
e.preventDefault();
var key = e.key;
const desk = desks.present[currentDesk];
if (editMode == true) {
switch (key) {
case 'ArrowLeft': //left
desk.x -= 5 / mapScale.width;
break;
case 'ArrowUp': //up
desk.y -= 5 / mapScale.height;
break;
case 'ArrowRight': //right
desk.x += 5 / mapScale.width;
break;
case 'ArrowDown'://down
desk.y += 5 / mapScale.height;
break;
default: break;
}
updateDesk(desk);
}
}
}
return <React.Fragment>{deskComponents}</React.Fragment>
}
export const Desk = forwardRef(({ desk, isEditing, scale, deskTypes, fns, index }, ref, onKeyDown) => {
const imgRef = useRef(null);
const [scl,] = useState(scale); //used to correct earlier issue - not necessary now
const [rotation, setRotation] = useState(desk?.rotation ?? 0);
const [size, setSize] = useState(() => {
let deskImg = null;
var dImg = deskTypes?.find(dsk => dsk.deskType === desk.deskType);
let top = parseInt(desk.y * scl.height);
const left = parseInt(desk.x * scl.width);
let width = 0;
let height = 0;
try {
if (dImg) {
deskImg = dImg.deskImage;
width = parseInt(dImg.wMM * scl.width);
height = parseInt(dImg.hMM * scl.height);
}
let imgStyle = {
boxSizing: "border-box",
width: `${width}px`,
height: `${height}px`,
}
const url = `data:image/jpeg;base64,${deskImg}`;
return { url: url, style: imgStyle, left: left ?? 0, top: top ?? 0 };
}
catch (ex) {
console.log(ex);
}
return { left: left, top: top };
});
const handleImageClick = (e) => {
e.stopPropagation();
fns.Set_Current(index);
};
useImperativeHandle(ref, () => ({
test: () => {
console.log('test');
},
img: ()=> imgRef.current
}));
return (
//located in a <Draggable container...>
<div >
<img ref={ref}
alt={`X=${size.left} Y=${size.top} ID=${desk.id} Rotation=${rotation}`}
src={size.url}
style={{
...size.style,
position: 'absolute',
transform: `rotate(${rotation}deg)`,
cursor: 'move',
}}
onClick={(e) => handleImageClick(e)} // Ensure clicks are handled
/>
</div>
)
});
As a thought, is this happening because the keydown event is called from the child, so is getting the child state and not the parent where the function is located?
EDIT
Recreated the problem in minimal code (as small as possible) in code sandbox
2
Answers
Since the Desk component’s
JSX
was not shared, I am assuming a<DIV>
or similar HTML element is the main container. Which you resize on keyDown. HTML element other than described in here https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event would not fireonKeyDown
event.You may try include
contenteditable
property in the resizing element.Example
JSX
.I have replicated your problem using a similar component with
<Div>
element and oncecontenteditable
property added and set the value totrue
thekeyDown
handler worked.Hope this helps!
Your main issue is that you have a stale closure over your state variables from the initial render:
Here you’re associating the original
keyDown
function from the first render scope with the keydown event (because your effect only runs after the initial render due to the empty dependency[]
). On future rerenders when your state changes, your event listener will continue to call the originalkeyDown
function which only knows about the state from the initial render (due to the closurekeyDown
has over the state variables when it was initially declared). You also need to treat your state (including nested state objects) as immutable (ie: read-only), meaning that you shouldn’t change your state usingdesk.x -= ...
etc. which changes your state object in place. You need to create a newdesk
object with the updated property.BBelow are a few different options which you can use to get around this:
Option 1: Define your effect’s dependencies
This issue mainly stems from the fact that you haven’t declared
keyDown
as a dependency in youruseEffect
dependency array. If you were to do that however, you’d have a new problem where youruseEffect
would be executing on every rerender as each new render creates a newkeyDown
function reference. You can minimise this impact by memoising your function (usinguseCallback()
) so that a new reference is only created when your state values within it change (allowing your function to see the latest state value):Your
useEffect()
also usesupdateSize
inside of it, so that should also technically be a dependency, so you may want to do the same exercise with that.Option 2: Use an effect event
This option is most likely the easiest solution to apply. Since this type of issue is quite a common occurrence, the React team have been looking at making this easier to manage with effect events. This introduces a new hook that allows your callback (ie:
keyDown
function) to always see the latest reactive values. This hook however is currently experimental and isn’t part of an official react version, so you’ll either need to import it like so:or create your own
useEffectEvent
custom hook that achieves similar (but not exactly the same) functionality:This hook returns a stable-reference to your function, meaning that its reference will always be the same across rerenders of your React component, allowing you to exclude it from your
useEffect()
dependency array. To use it, you can wrap yourkeyDown
function in it like so:Then leave your
useEffect()
as-is with an empty dependency array.One other thing that you’re doing that can potentially be causing issues is that you’re storing your Desk components in state. The reason why this is bad in your case is because you’re only creating those react element objects when your
useEffect()
callsbuildDesks()
, so those elements only know about the props and functions for when thatuseEffect()
was called rather than the ones in the current render. Typically to avoid this you would move your React elements outside of your state and do your.map()
when youreturn
from your component.