FrankTheTank
FrankTheTank

Reputation: 33

Issue with updating states in React Hooks

I am a bit confused and may not be using React Hooks correctly, but I started to build a site using functional components with React Hooks and I seem to have an issue with the updating of the states.

So here is my dilemma:

  1. I retrieve an array of items from an API and load it into a hook state which then gets mapped into a list group.

  2. When selecting a list item, an onClick event triggers and i load into another hook the selected item. I originally had it in one hook with nested objects within the state but decided to move it into its own so I can try to debug it better.

  3. Each input's value is tied to the state from the second hook (the selected item hook) and has an onChange function that sets the state for that specific hook. But whenever I start to type into the input field, the original list array somehow gets updated as I type. It seems that the onChange function is also linked to the original state/hook.

I am putting in some example code but i stripped out a lot of it due to there being a lot to read, but it is the basics of what I am trying to do.

const Terminal = props => {   const [state, setState] = useState({
    error: "",
    searchResults: [],
    showUploadModal: false,
    termList: [{
        _id: "",
        code: "",
        name: "",
        accessorial: {
            _id: "",
            detention: 0,
            chassispo: 0,
            chassisstd: 0,
            chassistri: 0,
            hazardous: 0,
            reefer: 0,
            howland: 0,
            liqour: 0,
            stopoff: 0,
            scale: 0,
            storage: 0,
            prepull: 0
        },
        active: true,
        add_date: ""
    }]   });

  let [selectedTerminal, setSelectedTerminal] = useState({
    selectedTerm:{
    name: "Test",
    code: "Test",
    accessorial: {
      detention: 0,
      chassispo: 0,
      chassisstd: 0,
      chassistri: 0,
      hazardous: 0,
      reefer: 0,
      howland: 0,
      liquor: 0,
      stopoff: 0,
      scale: 0,
      storage: 0,
      prepull: 0
    },
    active: false,
    add_date: {}   }   })

  useEffect(() => { 
    getTerminals();
    setFirstTerminal();   }, []);

  function getTerminals(){
    applicationApi
      .getTerminals()
      .then(resp => {
        setState({...state, termList: resp.terminals})
      })
      .catch(err => {
        throw err;
      });   }

  function onTerminalChange(term) {
    setSelectedTerminal({selectedTerm: term});   }

  function onChange(event){
    console.log(selectedTerminal);

    var {selectedTerm} = selectedTerminal;
    selectedTerm.name = event.target.value
    console.log(selectedTerm);
    setSelectedTerminal({selectedTerm: selectedTerm});   }

return (
    <>
      <Container fluid  style={{position: "fixed", width: "100%"}}>  
      <Row noGutters>
        <Col sm={2}>

          <ListGroup className="termList">
          {state.termList.map(term => {
            return <ListGroup.Item action onClick={() => onTerminalChange(term)}>
                      {term.name}{" - "}{term.code}
                    </ListGroup.Item>;
          })}
          </ListGroup>

        </Col>
        <Col sm={6}>
        <Tabs defaultActiveKey="main" id="uncontrolled-tab-example">
          <Tab eventKey="main" title="Main">
            <Form >
              <Form.Row>
                <Form.Group as={Col}>
                  <Form.Label>Name</Form.Label>
                  <Form.Control type="text" name="name" value={selectedTerminal.selectedTerm.name} onChange={(e) => onChange(e)}/>
                </Form.Group>

                <Form.Group as={Col}>
                  <Form.Label>Code</Form.Label>
                  {/* <Form.Control type="text" name="code" value={selectedTerminal.code}/> */}
                </Form.Group>
              </Form.Row>
        </Col>

      </Row>
    </Container>
    </>   ); };


export default (Terminal)

Here is a code sandbox with a similar issue. Its just changing the value once in the original list/state instead of just changing the other state/hook.

https://codesandbox.io/s/react-hook-issue-9rt83

Upvotes: 0

Views: 145

Answers (2)

hurricane
hurricane

Reputation: 6724

Instead of saving the state of object I would use the selected index.

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  let [state, setState] = useState({
    termList: [
      { name: "Adele", code: "ade" },
      { name: "Agnes", code: "agn" },
      { name: "Billy", code: "bil" },
      { name: "Calvin", code: "cal" },
      { name: "Christina", code: "chr" },
      { name: "Cindy", code: "cin" }
    ]
  });

  let [termIndex, setTermIndex] = useState(0);

  function onChangeName(event) {
    setState({
      termList: state.termList.map(term => ({
        ...term,
        name:
          event.target.defaultValue === term.name
            ? event.target.value
            : term.name
      }))
    });
  }

  function onChangeCode(event) {
    setState({
      termList: state.termList.map(term => ({
        ...term,
        code:
          event.target.defaultValue === term.code
            ? event.target.value
            : term.code
      }))
    });
  }

  return (
    <div className="App">
      <h2>
        Click on any of the items to fill in inputs. Then type into inputs to
        see issue.
      </h2>
      <input
        name="name"
        value={state.termList[termIndex].name}
        onChange={onChangeName}
      />
      <input
        name="code"
        value={state.termList[termIndex].code}
        onChange={onChangeCode}
      />
      <ul>
        {state.termList.map((term, index) => {
          return (
            <li onClick={() => setTermIndex(index)}>
              {term.name + " - " + term.code}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

https://codesandbox.io/s/react-hook-issue-fvqsr?fontsize=14&hidenavigation=1&theme=dark

Upvotes: 0

xadm
xadm

Reputation: 8418

It seems that the onChange function is also linked to the original state/hook.

yes ... technically it's a ref to the same object ... change (mutation) is done on exactly the same, one object ... no matter which one is modified

To fix you need to copy all source object properties into new one (as usual in setState) - should be a deep clone for complex structures.

In this case (2 levels) simple fix should be enough:

function onTerminalChange(term) {
  const newTerm = {...term};  // new main object
  newTerm.accessorial = {...term.accessorial}; // new subobject
  setSelectedTerminal({ selectedTerm: newTerm });
}

for codesandbox:

        <li onClick={() => setTermState( {...term} )}>

Upvotes: 1

Related Questions