Why is React component losing state sometime between setting state and keydown?

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

3

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.

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 execute 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.

2

Hi I think your issue is how you are updating the state when you use setState in event listeners the event listeners will have the state when the event listener is initialized

1: update your state by passing a callback to the setState instead of a value

updateDesk((prevState) => {...prevState, x: prevState.x - (5 / mapScale.width) });

2:

useEffect(() => {
    window.addEventListener('resize', updateSize);
    window.addEventListener('keydown', keyDown);
    return () => {
        window.removeEventListener('resize', updateSize);
        window.removeEventListener('keydown', keyDown);
    }
}, [(add the values which you are taking a reference inside the setState)])

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật