0

I'm trying to use a react modal to allow someone to change a value and have the modal buttons be dynamic such that initially there are Cancel / Submit buttons, but after Submit is pressed and the value is changed, the buttons are replaced with a Close button.

The problem I am having is that using "{modalButtonGroup}" in the modal results in "tmpName" being undefined when "handleSetNameSubmit" is called. If I instead comment out the "{modalButtonGroup}" line and just use hard coded buttons (which are currently commented out in the below code), then "tmpName" is set correctly when "handleSetNameSubmit" is called.

Is there some aspect of state context that causes "tmpName" to not be known when "{modalButtonGroup}" is used?

import { useState, useEffect } from 'react';
import { Row, Table, Form, Button, Modal, Alert } from 'react-bootstrap';

const System = () => {
  const [tmpName, setTmpName] = useState();

  const [showName, setShowName] = useState(false);

  const handleClose = () => {
    setShowName(false);
  }

  const handleCancel = () => {
    setShowName(false);
  };

  const handleSetNameSubmit = () => {
    console.log('tmpName: ', tmpName);
    //code to change the name to tmpName
    setModalButtonGroup(modalButtonsPostSubmit);
  }

  const modalButtonsPreSubmit = () => {
    return (
      <>
        <Button variant="secondary" onClick={handleCancel}>
          Cancel
        </Button>
        <Button variant="primary" onClick={handleSetNameSubmit}>
          Submit
        </Button>
      </>
    )
  };

  const modalButtonsPostSubmit = () => {
    return (
    <>
      <Button variant="secondary" onClick={handleClose}>
        Close
      </Button>
    </>
    )
  };
  const [modalButtonGroup, setModalButtonGroup] = useState(modalButtonsPreSubmit);

  return (
    <>
    <div className="card">
      <h5>System</h5>
        <Table variant="dark" responsive>
          <tr>
            <td>Name:</td>
            <td>Name <Button onClick={() => setShowName(true)}>Edit</Button></td>
          </tr>
        </Table>
    </div>

    {/* Set Name */}
    <Modal show={showName} onHide={handleClose}>
      <Modal.Header closeButton>
        <Modal.Title>Set Name</Modal.Title>
      </Modal.Header>
      <Modal.Body>
        <span>
          <Form.Control
            type="text"
            defaultValue=name
            onChange={(event) => setTmpName(event.target.value)}
          />
        </span>
      </Modal.Body>
      <Modal.Footer>
        {modalButtonGroup}
        {/*<Button variant="secondary" onClick={handleCancel}>*/}
        {/*  Cancel*/}
        {/*</Button>*/}
        {/*<Button variant="primary" onClick={handleSetNameSubmit}>*/}
        {/*  Submit*/}
        {/*</Button>*/}
      </Modal.Footer>
    </Modal>
}

export default System;

UPDATE, I tried updating the code per suggestion as follows but now no buttons are appearing at all.

import { useState, useEffect } from 'react';
import { Row, Table, Form, Button, Modal, Alert } from 'react-bootstrap';

const System = () => {
  const [tmpName, setTmpName] = useState();

  const [showName, setShowName] = useState(false);
  
  const [submitted, setSubmitted] = useState(false);

  const handleClose = () => {
    setShowName(false);
  }

  const handleCancel = () => {
    setShowName(false);
  };

  const handleSetNameSubmit = () => {
    console.log('tmpName: ', tmpName);
    //code to change the name to tmpName
    setSubmitted(modalButtonsPostSubmit);
  }

  const modalButtonsPreSubmit = () => {
    return (
      <>
        <Button variant="secondary" onClick={handleCancel}>
          Cancel
        </Button>
        <Button variant="primary" onClick={handleSetNameSubmit}>
          Submit
        </Button>
      </>
    )
  };

  const modalButtonsPostSubmit = () => {
    return (
    <>
      <Button variant="secondary" onClick={handleClose}>
        Close
      </Button>
    </>
    )
  };
  const buttons = submitted ? modalButtonsPostSubmit : modalButtonsPreSubmit;

  return (
    <>
    <div className="card">
      <h5>System</h5>
        <Table variant="dark" responsive>
          <tr>
            <td>Name:</td>
            <td>Name <Button onClick={() => setShowName(true)}>Edit</Button></td>
          </tr>
        </Table>
    </div>

    {/* Set Name */}
    <Modal show={showName} onHide={handleClose}>
      <Modal.Header closeButton>
        <Modal.Title>Set Name</Modal.Title>
      </Modal.Header>
      <Modal.Body>
        <span>
          <Form.Control
            type="text"
            defaultValue=name
            onChange={(event) => setTmpName(event.target.value)}
          />
        </span>
      </Modal.Body>
      <Modal.Footer>
        {buttons}
        {/*<Button variant="secondary" onClick={handleCancel}>*/}
        {/*  Cancel*/}
        {/*</Button>*/}
        {/*<Button variant="primary" onClick={handleSetNameSubmit}>*/}
        {/*  Submit*/}
        {/*</Button>*/}
      </Modal.Footer>
    </Modal>
}

export default System;

1 Answers1

1

Here's what's happening. It's kind of complicated because of the unusual way in which you have written your component. I'll suggest a simpler way to do it below, but it might be educational to unpack what's going on.

  1. Your <System> component renders for the first time:
    • tmpName is undefined
    • the handleNameSubmit function is generated and it "closes over" the current value of tmpName. This means every time this particular function value is called, it will always console.log 'tmpName: undefined'. See background on JavaScript closures
    • the modalButtonsPreSubmit function is generated and it closes over the current value of handleNameSubmit and binds this value to the submit button click event.
    • Then, you pass the modalButtonsPreSubmit function as the initial value of a useState hook. The way that useState works, this initial value is only used in the first render (see docs). The modalButtonsGroup value returned by this useState call will be frozen to this particular value (with all the closures) through subsequent re-renders, until you change it by calling setModalButtonsPreSubmit with a new function.
  2. The user types some text in the textbox. For each character your onChange handler calls setTempName, which triggers the <System> component to re-render with a new value in the tmpName state. However, modalButtonsPreSubmit is still frozen to what it was in the first render.
  3. The user clicks "Submit", which triggers the version handleNameSubmit that was generated on the first render, when tmpName was undefined.

The way to simplify things so that it works as expected is to not store functions in state. That way they'll get re-generated on each re-render with fresh values for any other state that they reference.

So instead of..

const modalButtonsPreSubmit = () => (
   <> {/* Markdown for "Submit" and "Cancel" buttons */} </>
);

const modalButtonsPostSubmit = () => (
   <> {/* Markdown for "Close" button */} </>
);

const [modalButtonGroup, setModalButtonGroup] = useState(modalButtonsPreSubmit);

return (
   <div>
      {/* The rest of the app */}
      {modalButtonGroup}
   </div>
);

You'd do something like this...

const [submitted, setSubmitted] = useState(false);
const buttons = submitted ? 
    <> {/* Markdown for "Close" button */} </> : 
    <> {/* Markdown for "Submit" and "Cancel" buttons */} </>;

return (
   <div>
      {/* The rest of the app */}
      {buttons}
   </div>
);

See this codesandbox for a working solution.

Andrew Stegmaier
  • 3,429
  • 2
  • 14
  • 26
  • Thanks for the detailed response. I tried this approach (see my revised post) but now the buttons aren't displaying at all. Did I miss a critical part of your suggested change? – EndlesKinetic Oct 22 '21 at 19:02
  • You're almost there. You just need to change the `modalButtonsPreSubmit` and `modalButtonsPostSubmit` variables from functions that return JSX to just simple JSX. (e.g. instead of `const modalButtonsPreSubmit = () =>
    something
    `, just write `const modalButtonsPreSubmit =
    something
    `. See [this codesandbox](https://codesandbox.io/s/stackoverflow-rendering-with-closures-solution-o44o0?file=/src/App.js)
    – Andrew Stegmaier Oct 22 '21 at 19:09
  • Glad I could help! If you're satisfied that my answer solved your problem, would you mind hitting the checkbox next to the answer to mark it as "accepted"? That helps others know that you considered the answer the right one, and it's a nice way of saying "thank you" :-) – Andrew Stegmaier Oct 22 '21 at 19:43
  • Accepted, by the way, are there any better ways to approach this problem that you'd recommend beyond what you suggested already? – EndlesKinetic Oct 22 '21 at 20:41
  • It think you're on the right track. Two other improvements that come to mind are (1) you probably only need a single `close` function instead of two functions (`handleClose` and `handleCancel`) that do the same thing. And (2) instead of setting ` – Andrew Stegmaier Oct 23 '21 at 01:04