0

I have a form where users create a coding problem. In the form, there is the option to add sample test cases via input and output text boxes. The user can click a button to add a new test case. Right now I have a state object that holds all of the form data, formObj, and within it is a sample_test_cases field which I want to hold an array of objects, like: [{ input: "", output: "" }].

The issue I am having is updating this array. I need to be able to concat a new object to it each time a test case is added. Then updating the state at that index when the text box is changed. I have tried creating a stateless array and updating it, then setting sample_test_cases to that array. This is not working, however.

Here is a sandbox with my code: https://codesandbox.io/s/eloquent-snowflake-n4bpl?file=/src/AddProblemForm.js

If anyone could give me tips that would really help. I'm not too familiar with Javascript or complex state management. Thanks.

keroana
  • 69
  • 4

2 Answers2

0

See snippet below (I'm having issues with saving the sandbox). A few issues that it addresses:

  1. You also kept track of state using some local arr variable; updating that won't trigger a re-render, so you'll need to modify the sample_test_cases array inside your formObj state.
  2. The textarea value should also be reflecting what is in your state. To make this more convenient, I've passed the test case into the SampleTestCase component as a prop, so it will react to state changes.
  3. React state should be treated as immutable. So when the input text box is updated, you should set the state to a new object with a new sample_test_cases array, which is constructed of the first i-1 test cases, a new ith test case with modified input, and the remaining test cases.
  4. Move SampleTestCase outside of the AddProblemForm component. If you don't, you will find that whenever the input textarea is changed, you will lose keyboard focus. This is because the SampleTestCase component is being redefined on each render, which is triggered by a state change. (Similar problem: React.js - input losing focus when rerendering)
import React, { useState } from "react";
import { Form, Button, Col } from "react-bootstrap";
import { BsPlusSquare } from "react-icons/bs";

const SampleTestCase = ({ testCase, updateInput }) => {
  return (
    <Form.Row>
      <Col>
        <Form.Group controlId="input">
          <Form.Label>Sample Input</Form.Label>
          <Form.Control
            required
            as="textarea"
            rows={2}
            onChange={updateInput}
            value={testCase.input}
          />
        </Form.Group>
      </Col>
      <Col>
        <Form.Group controlId="output">
          <Form.Label>Sample Output</Form.Label>
          <Form.Control
            required
            as="textarea"
            rows={2}
            // onChange={(event) =>
            //   setFormObj({
            //     ...formObj,
            //     sample_test_cases: {
            //       ...formObj.sample_test_cases,
            //       output: event.target.value
            //     }
            //   })
            // }
          />
        </Form.Group>
      </Col>
    </Form.Row>
  );
};

const AddProblemForm = () => {
  const [formObj, setFormObj] = useState({
    sample_test_cases: [{ input: "", output: "" }]
    // other state obj info
  });

  const AddSampleTestCase = () => {
    // make new instance!
    const newTestCase = { input: "", output: "" };
    setFormObj({
      ...formObj,
      sample_test_cases: [...formObj.sample_test_cases, newTestCase]
    });
  };

  console.log(formObj);

  const updateInputFor = (i) => (event) => {
    event.preventDefault();

    const { sample_test_cases } = formObj;
    const testCase = sample_test_cases[i];
    testCase.input = event.target.value;

    setFormObj({
      ...formObj,
      sample_test_cases: [
        ...sample_test_cases.slice(0, i),
        testCase,
        ...sample_test_cases.slice(i + 1)
      ]
    });
  };

  return (
    <div>
      Problem Form
      <Form>
        <h5 style={{ paddingTop: "1rem" }}>Sample Test Cases</h5>
        {formObj.sample_test_cases.map((testCase, i) => (
          <React.Fragment key={i}>
            <SampleTestCase
              key={i}
              testCase={testCase}
              updateInput={updateInputFor(i)}
            />
            <hr />
          </React.Fragment>
        ))}
        <Button onClick={AddSampleTestCase}>
          <div>Add Sample Test Case</div>
        </Button>
      </Form>
    </div>
  );
};

export default AddProblemForm;
Anson Miu
  • 1,171
  • 7
  • 6
  • Sandbox works now: https://codesandbox.io/s/brave-shannon-fto3p?fontsize=14&hidenavigation=1&theme=dark&file=/src/AddProblemForm.js – Anson Miu Apr 08 '21 at 20:36
0

I suggest you use useReducer hook in cases your state is complicated.

hooks API

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

AddProblemForm.js:

import React, { useReducer } from "react";
import { Form, Button, Col } from "react-bootstrap";
import { BsPlusSquare } from "react-icons/bs";
import SampleTestCase from "./SampleTestCase";

const initialState = {
  sample_test_cases: [],
  counter: 0
  // other state obj info
};
function reducer(state, action) {
  switch (action.type) {
    case "addSampleTestCase": {
      const { data } = action;
      return {
        ...state,
        sample_test_cases: [...state.sample_test_cases, data],
        counter: state.counter + 1
      };
    }
    case "updateTest": {
      const { index, value } = action;
      return {
        ...state,
        sample_test_cases: state.sample_test_cases.map((item, i) => {
          if (i === index) {
            return value;
          } else {
            return item;
          }
        })
      };
    }

    default:
      throw new Error();
  }
}

const AddProblemForm = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const AddSampleTestCase = () => {
    dispatch({ type: "addSampleTestCase", data: { input: "", output: "" } });
  };

  /* console.log(state); */

  return (
    <div>
      Problem Form
      <Form>
        <h5 style={{ paddingTop: "1rem" }}>Sample Test Cases</h5>
        {state.sample_test_cases.map((sample_test_case, i) => (
          <div key={i}>
            <SampleTestCase
              sample_test_case={sample_test_case}
              updateValue={(value) =>
                dispatch({ type: "updateTest", index: i, value })
              }
            />
            <hr />
          </div>
        ))}
        <Button onClick={AddSampleTestCase}>
          <div>Add Sample Test Case</div>
        </Button>
      </Form>
    </div>
  );
};

export default AddProblemForm;

SampleTestCase.js:

import React from "react";
import { Form, Button, Col } from "react-bootstrap";

const SampleTestCase = ({ sample_test_case, updateValue }) => {
  return (
    <Form.Row>
      <Col>
        <Form.Group controlId="input">
          <Form.Label>Sample Input</Form.Label>
          <Form.Control
            required
            as="textarea"
            rows={2}
            value={sample_test_case.input}
            onChange={(event) => updateValue(event.target.value)}
          />
        </Form.Group>
      </Col>
      <Col>
        <Form.Group controlId="output">
          <Form.Label>Sample Output</Form.Label>
          <Form.Control
            required
            as="textarea"
            rows={2}
            value={sample_test_case.output}
            onChange={(event) => updateValue(event.target.value)}
          />
        </Form.Group>
      </Col>
    </Form.Row>
  );
};
export default SampleTestCase;
Vahid18u
  • 393
  • 1
  • 4
  • 12