React Hooks: useState with Arrays

I spent a good portion of the work day today trying to code up a simple form to accept a list of zip codes and put them in an array.

The final form looks as follows:

As you can see there are a few zip codes and then we can click the ‘Add Zip Code’ button and we get a new set of inputs.

To make this work, our form code looks as follows:

import React, {useState} from 'react';
import {
  Button,
  ButtonGroup,
  Col,
  Form,
  FormGroup,
  FormText,
  Input,
  Label,
  Row,
} from 'reactstrap';
const MyForm = ({formSubmitFunc}) => {
  const [zipCodes, setZipCodes] = useState([""]);
  const [costs, setCosts] = useState([""]);

  return(
    <Form className="form" onSubmit={formSubmitFunc}>
    { Array.apply(null, {length: zipCodes.length}).map( (row, index) =>
      <Row key={index}>
        <Col>
          {console.log("row: ", row, " index: ", index, "value: ", zipCodes[index] )}
          <FormGroup>
            <Label>Zip Codes</Label>
            <Input
              type="input"
              value={zipCodes[index]}
              onChange={e => {setZipCode(index, e.target.value)}}/>
            <FormText>What Zip Codes will you service?</FormText>
          </FormGroup>
        </Col>
        <Col>
          <FormGroup>
            <Label>Cost for service in this Zip Code</Label>
            <Input
              type="input"
              value={costs[index]}
              onChange={(e) => setCost(index, e.target.value)}/>
            <FormText>How much will you charge in this Zip Code for your service?</FormText>
          </FormGroup>
        </Col>
        <Col className="align-self-center">
        { index === (zipCodes.length - 1) ?
            <ButtonGroup>
              <Button color="primary" onClick={addRow}>Add Zip Code</Button>
              {index !== 0 &&
                <>
                  &nbsp;
                  <Button color="secondary" onClick={removeRow}>Remove Zip Code</Button>
                </>
              }
            </ButtonGroup>
            :
            <div>&nbsp;</div>
          }
        </Col>
      </Row>
    )}
    </Form>
  );
}

This part was pretty standard and I had no problem. The issue, however comes from the following functions: setZipCode, setCode , addRow, removeRow.

The standard way I thought it could work was to do something like the following in setZipcode

const setZipCode = (index, value) => {
    let c = zipCodes;
    c[index] = value;
    setZipCodes(c);
  }

This, however, doesn’t work at all. It seems like it should: You get the list of current zipCodes (which is a state), assign it to another variable. Then modify that variable. Next, you setZipCodes to that new variable c.

Turns out this just doesn’t have the expected behavior. It won’t update because it looks as if nothing has changed. Instead, we need to use the spread operator. This makes it seem like its getting a brand new array and then updates.

const setZipCode = (index, value) => {
    let c = zipCodes;
    c[index] = value;
    setZipCodes(c => [...c]);
  }

We can then use these spread operators on the other three functions:

const setCost = (index, value) => {
    const c = costs;
    c[index] = value;
    setCosts(c => [...c]);
}
const addRow = (e) => {
    // add zip code row for service.
    var z = zipCodes;
    setZipCodes(oldArray => [...oldArray, z[z.length - 1]]);

    var c = costs;
    setCosts(oldCosts => [...oldCosts, c[c.length - 1]]);
}
const removeRow = (e) => {
    setZipCodes(oldArray => [...oldArray.slice(0, oldArray.length - 1)]);
    setCosts(oldArray => [...oldArray.slice(0, oldArray.length - 1)]);
}

Now our form gets the expected behavior and we’re able to add new zip codes as well as edit them.

An alternative would be to just have one array full of objects [{zip: 98611, cost: $30}, {zip: 97035, cost: $30}] and have one useState for this.

Hopefully that helps someone save time as I burned a few hours trying to figure this out!

Leave a Reply

Your email address will not be published. Required fields are marked *