ReactJS w/useReducer - input components are frozen despite change handler - sometimes

18 Views Asked by At

I would like to make, using React, a component called CountBox that could allow a user to edit the length and contents of an array held by its parent with useReducer. The CountBox consists of a number input for the length of the array and a set of text input boxes, one for each element, which are removed when the array is shorter and added when the array is longer. The component has its own useState hook holding an Array of the text input boxes. Behavior is as desired - except:

  • When the array is not initially empty, in which case the correct number of prepopulated boxes appears, but these boxes do not allow one to edit their contents. Newly created boxes do behave as expected, and shortening the list and re-lengthening it populates it with functional boxes.

  • The number input does not allow for normal typing but keystrokes in it do change the state and the number of text inputs as expected (e.g., typing "0" when a "1" is present produces 10 text boxes, but the shown value becomes "2"). Its value property is set to the length of the state list, which also becomes 10 in the example.

How can I make the visible contents match the state? Sample code is shown below - this was shrunk from a larger project when the project was encountered, but is still a whole App.js, because I don't know where things would best be changed.

import { React } from 'react';
import { useState } from 'react';
import { useReducer } from 'react';

// Input consisting of simple text box with value and change handler
// num is the ordinal - which element in the main state array it corresponds to
function InputBox( { num, theList, handleInputChange } ) {
  return(<input key={'input_'+num} id={'input_'+num} value={theList[num]} onChange={ (e) => { handleInputChange(e.target.value, num); } } />);
}

// Component to produce array of InputBoxes with counter
// the layer_content argument indicates some React function with props num, theList, and handleInputChange,
// which in this case is InputBox.
function CountBox( { name, label, theList, handleCountChange, top_content, layer_content, layerContentHandler } ) {
  // This component has a state containing an Array of its child InputBoxes, which is displayed in
  const [getChildren, setChildren] = useState(
    new Array(theList.length).fill(1).map( (val, ind) => { return(<div key={ind}>{layer_content( {"num":ind, "theList":theList, "handleInputChange":layerContentHandler } )}</div>); } ));

  // Function to change the length of the Array of InputBoxes.
  const changeChildren = (ct) => {
    let count = getChildren.length;
    if (count > ct & ct > 0) {
      setChildren(getChildren.slice(0, ct)); // Shortening is easy.
    }
    if (count < ct) {
      let extras = [];
      while (count < ct) { // Lengthening is harder
        extras.push( <div key={count}>{layer_content( {"num":count, "theList":theList, "handleInputChange":layerContentHandler } )}</div>);
        ++count;
      }
      setChildren([...getChildren, ...extras]);
    }
  }

  // Assemble the component - no formatting for now.
  return(<div>
    {top_content}
    <label for={name}>{label}</label>
    <input type="number" step="1" min="1" name={name} value={theList.length}
      onChange={ (e) => { handleCountChange(e.target.value); changeChildren(e.target.value); } }></input>
    {getChildren}
  </div>)
}

// Reducer for two functions - change count and change value
function listReducer(list, action) {
  switch (action.type) {
    case 'count':
      if (action.ct > list.length) return [...list, new Array(action.ct - list.length).fill("")];
      else return list.slice(0, action.ct);
    case 'value':
      return list.map( (v, i) => i === action.ind ? action.val : v );
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

// App itself
export default function App() {
  const [theList, dispatch] = useReducer(listReducer, ['Monday','Tuesday']);
  function handleCountChange(ct) {
    dispatch( { type: 'count', ct: ct });
  }
  function handleValueChange(val, ind) {
    dispatch( { type: 'value', ind: ind, val: val});
  }

  function alertList() {
    alert(`theList: ${theList}`);
  }

  return (
    <div>
      <CountBox name="test" label="Count: " top_content={<h1>Testing</h1>} theList={theList}
        layer_content={InputBox} handleCountChange={handleCountChange} layerContentHandler={handleValueChange}/>
      <button onClick={alertList}>Check state</button>
    </div>
  );
}

As I am new to React and to StackOverflow, I will try to appreciate any and all comments regarding formatting/practices as well.

I have tried:

  • Previously, something similar with only the useState hook, which I also expected to work; however, asychronous modifications to the state canceled each other out and were worse, so I would like to stick with useReducer.
  • Placing useEffect hooks in various locations, with various attempts to manually reset the value of components when state changes or just to log something. Usually it violated the Rules of Hooks.
  • Instead of handing layer_content to the CountBox via props, writing <InputBox num={ind} ... /> where layer_content was called. No improvement in behavior, and it's less reusable.
  • Ensuring my change handlers update state in the expected way, and are present on all controlled components.
  • Writing defaultValue={theList[num]} instead of value for the inputs. Technically works, but it isn't what's typically used; it isn't necessary in other cases; and it won't allow me to change state from other components, which is something I would like to do in the larger implementation.
0

There are 0 best solutions below