javascriptreactjsnext.jsbootstrap-modal

React: Array inside of State is undefined when accessed from callback


I have a callback function, that is supposed to edit a value in an array, that is inside of a State. When I try to access the dataZähler, so the array itself, I get unedefined/Array of length 0 back. Why is that?

Code in Question(hopefully reduced to its bare necessities):

'use client'
import { useCallback, useEffect, useState } from "react";
import { ProtokollEintrag, TogglePosition, ZählerCallback, ZählerObjekt } from "./dataTypes";
import { ToggleButton, ToggleButtonGroup, Divider, ZoomProps } from "@mui/material";
import ElectricalServicesIcon from '@mui/icons-material/ElectricalServices';
import clsx from "clsx";
import { Modal } from "react-bootstrap";
import { CreateCounterPopUp, CreateEntryPopUp, EditCounterPopUp, EditEntryPopUp, CalculateDeltaPopUp } from "./pages/myPopups";

export default function Home() {
  //data and internal logic states, table-content, which rows are selected, is the data still loading, etc.
  const [loading, setLoading] = useState(true);
  const [dataZähler, setDataZähler] = useState<ZählerObjekt[]>([]);
  const [dataProtokoll, setDataProtokoll] = useState<ProtokollEintrag[]>([]);
  const [selectedRowZähler, setSelectedRowZähler] = useState(-1);
  const [selectedRowProtokoll, setSelectedRowProtokoll] = useState(-1);
  //View-Logic states, which table is shown, if modals should be seen, etc
  const [showEditZählerModal, setShowEditZählerModal] = useState(false);

  //on start, load both csv's into states
  useEffect(() => {
    fetch('/api/loadCsv')
      .then((res) => res.json())
      .then((data) => {
        setDataZähler(data.dataZähler)
        setDataProtokoll(data.DataProtokoll)
        setLoading(false)
      })
  }, []);

  function handleEditZähler() {
    if (currentTable === TogglePosition.Zähler && selectedRowZähler >= 0) {
      setShowEditZählerModal(true);
    }
  };

  const handleEditZählerCallback: ZählerCallback = useCallback((data: ZählerObjekt | null) => {
    console.log(dataZähler);
    if (data) {
      const oldData = dataZähler.map((elem, idx) => {
        if (idx === selectedRowZähler)
          return { ...data };
        return elem;
      });
      console.log(data);
      console.log(oldData);
      setDataZähler(oldData);
      setSelectedRowZähler(-1);
    }
    setShowEditZählerModal(false);
  }, []);

  return (
    <div className="flex flex-col">
      {/* Buttons und Toggle zur Verwendung und Steuerung des Interfaces */}
      <div className="justify-center flex sticky bottom-0 space-x-4 bg-[white] bg-opacity-60">
      <button onClick={handleEditZähler} disabled={selectedRowZähler < 0} className={clsx("bg-[#0ea5e9] border-[none] text-[white] text-center inline-block text-base rounded-[5px] px-5 py-3 my-2", { "bg-[#d1d5db]": selectedRowZähler < 0 })}>Bearbeiten</button>
      </div>
      {/*Modal Div - all edit/Cancel/Save/Create/Delete modals go here*/}
      <div>
        <Modal size="lg" show={showEditZählerModal} onHide={() => setShowEditZählerModal(false)}>
          <Modal.Header closeButton>
            <Modal.Title>
              Zähler Bearbeiten:
            </Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <EditCounterPopUp data={dataZähler[selectedRowZähler]} callback={handleEditZählerCallback} />
          </Modal.Body>
        </Modal>
      </div>
    </div>
  );
}

and if it is important, here is the Modal for Editing the ZählerObjekt:

import { ChangeEvent, FormEvent, useState } from "react";
import { ZählerObjekt, ProtokollEintrag, ZählerCallback, EintragCallback } from "../dataTypes";
import { Form } from "react-bootstrap";
import { generateGenericObjectID } from "../util/objectIdentifier";

interface CounterProps {
    data: ZählerObjekt,
    callback: ZählerCallback
}

export function EditCounterPopUp({ data, callback }: CounterProps) {
    const [newCounter, setNewCounter] = useState({ ...data });

    function handleSpeichern(event: React.MouseEvent<HTMLButtonElement>) { //dont forget to set the id of the new object
        event.preventDefault();
        setNewCounter({ ...newCounter, ...{ id: generateGenericObjectID(newCounter) } });
        callback(newCounter);
    }

    function handleInputChange(field: string, event: ChangeEvent<HTMLInputElement>) {
        const newState = { ...newCounter };
        newState[field as keyof ZählerObjekt] = event.target.value;
        setNewCounter(newState);
    }

    return (
        <div>
            <form>
                {Object.entries(newCounter).slice(1).map((elem, idx) => {
                    return (
                        <Form.Group
                            className="mb-3" key={idx} controlId={elem[0]}
                        >
                            <Form.Label>{elem[0]}</Form.Label>
                            <Form.Control
                                as="textarea"
                                rows={2}
                                placeholder={"z.B. " + String(data[elem[0] as keyof ZählerObjekt])}
                                value={elem[1]}
                                onChange={(e) => handleInputChange(elem[0], e as any)} />
                        </Form.Group>
                    );
                })}
                <div className="float-right">
                    <button onClick={handleSpeichern} className="bg-[#22c55e] border-[none] text-[white] text-center inline-block text-base rounded-[5px] px-5 py-3 my-2">Speichern</button>
                </div>
            </form>
        </div>
    );
}

Why is dataZähler unedfined in the callback? and how can I fix this?


Solution

  • The issue is an empty dependency array. Include dataZähler and selectedRowZähler in the dependency array of useCallback. Check the code below:

    const handleEditZählerCallback: ZählerCallback = useCallback((data: ZählerObjekt | null) => {
      if (data) {
        const oldData = dataZähler.map((elem, idx) => {
          if (idx === selectedRowZähler)
            return { ...data };
          return elem;
        });
        setDataZähler(oldData);
        setSelectedRowZähler(-1);
      }
      setShowEditZählerModal(false);
    }, [dataZähler, selectedRowZähler]);
    

    The dependencies array [dataZähler, selectedRowZähler] ensures that the callback has the most recent values of these dependencies every time they change. Without this, the callback may use stale values of dataZähler and selectedRowZähler.