skip to Main Content

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

https://codesandbox.io/p/sandbox/xenodochial-pine-t6ccl8

2

Answers


  1. 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 fire onKeyDown event.

    You may try include contenteditable property in the resizing element.

    contentEditable="true"
    

    Example JSX.

     <div className={styles.rangeDrawerMain} style={dynamicStyle} key={id} id={id}>
           <div className={styles.appSubTitle} style={titleStyle}>{props.title}</div>
           <div style={containerStyle} >                
                <drawer style={drawerStyle} id={rangeDrawerId} ref={drawerRef} width={width} height={height} />    
                <div contentEditable="true" id={drawerKnob} onKeyDown={keyDown} onMouseDown={activeDraw} style={buttonStyle}></div>                
           </div>
    </div>
    

    I have replicated your problem using a similar component with <Div> element and once contenteditable property added and set the value to true the keyDown handler worked.

    Hope this helps!

    Login or Signup to reply.
  2. Your main issue is that you have a stale closure over your state variables from the initial render:

    useEffect(() => {
        ...
        window.addEventListener('keydown', keyDown);
        return () => {
           ...
            window.removeEventListener('keydown', keyDown);
        }
    }, [])
    

    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 original keyDown function which only knows about the state from the initial render (due to the closure keyDown 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 using desk.x -= ... etc. which changes your state object in place. You need to create a new desk object with the updated property.B

    Below 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 your useEffect dependency array. If you were to do that however, you’d have a new problem where your useEffect would be executing on every rerender as each new render creates a new keyDown function reference. You can minimise this impact by memoising your function (using useCallback()) so that a new reference is only created when your state values within it change (allowing your function to see the latest state value):

    const updateDesk = useCallback((desk) => { // wrapping this in a useCallback means this function reference is only created once
      // This now no longer mutates your state directly
      setDesks((currentDesks) => ({
        ...currentDesks,
        present: currentDesks.present.map(d => d.id === desk.id ? desk : d)
        future: []
      }));
    }, []);
    
    const keyDown = useCallback(function(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
              updateDesk({...desk, x: desk.x - (5 / mapScale.width) }); // no longer mutating your state, instead, creating a new object with the property updated
              break;
            case 'ArrowUp': //up
              updateDesk({...desk, y: desk.y - (5 / mapScale.height) });
              break;
            case 'ArrowRight': //right
              updateDesk({...desk, x: desk.x + (5 / mapScale.width) });
              break;
            case 'ArrowDown': //down
              updateDesk({...desk, y: desk.y + (5 / mapScale.height)});
              break;
            default:
              break;
          }
        }
      }
    }, [currentDesk, editMode, desks.present, updateDesk]);
    
    useEffect(() => {
        ...
        window.addEventListener('keydown', keyDown);
        return () => {
           ...
            window.removeEventListener('keydown', keyDown);
        }
    }, [keyDown]);
    

    Your useEffect() also uses updateSize 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:

    import { experimental_useEffectEvent as useEffectEvent } from 'react';
    

    or create your own useEffectEvent custom hook that achieves similar (but not exactly the same) functionality:

    // Rough example of how `useEffectEvent` could be implemented.
    // Taken from: https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#internal-implementation
    function useEffectEvent(handler) {
      const handlerRef = useRef(null);
      useLayoutEffect(() => {
        handlerRef.current = handler;
      });
      return useCallback((...args) => {
        const fn = handlerRef.current;
        return fn(...args);
      }, []);
    }
    

    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 your keyDown function in it like so:

    const keyDown = useEffectEvent(function(e) {
      if (currentDesk > -1 && editMode == true) {
        e.preventDefault();
        /* ... rest of your keyDown implementation ... */
       }
    });
    

    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() calls buildDesks(), so those elements only know about the props and functions for when that useEffect() 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 you return from your component.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search