0

I have a react component that will accept an arbitrary number of files, each with its own language. I want to be able to have a button that lets a user add as many files as they want, each with its own dropdown menu of languages. I've eliminated the file upload for now as I isolate the issue I'm having.

I've created quite a few similar components already without a problem, but for some reason I'm having major trouble with this one. When I change the language on the dropdown, I console log state and see that the language has updated in state, but it does not update on the DOM in my map method.

Here is my code

function App() {
  const [files, setFiles] = useState([]);

  const languages = ["English", "Spanish", "Arabic", "French"];

  const handleAddFile = e => {
    let newFiles = files;
    newFiles.push({
      content: null,
      preview: null,
      language: ""
    });
    setFiles(newFiles);
  };

  const changeLanguage = (event, index) => {
    let newFiles = files;
    newFiles[index].language = event.target.value;
    setFiles(newFiles);
  };

  console.log(files);

  return (
    <div>
      <p>Files</p>
      <p>Upload a file for as many languages as you have</p>
      {files.map((file, index) => (
        <div key={index}>
          <p>current language: {file.language}</p>
          <select
            value={file.language}
            onChange={event => changeLanguage(event, index)}
          >
            <option value="" disabled>
              Select a language
            </option>
            {languages.map((lang, i) => (
              <option value={lang} key={i}>
                {lang}
              </option>
            ))}
          </select>
        </div>
      ))}
      <button onClick={e => handleAddFile(e)}>+ Add File</button>
    </div>
  );
}

I've also made a codesandbox to reproduce the issue, but it actually further complicated things. When I run the code locally, I am able to add file instances just fine, but the language on the select element does not update on the DOM for any instance. In the codesandbox, nothing updates on the DOM unless I make an edit in the editor, then everything updates just as I'd expect, including the language. In all cases state is console logged exactly as I'd expect it to be. The component in the sandbox is identical to what I have locally, though locally it is embedded in another component.

https://codesandbox.io/s/ed4fx?file=/src/App.js:0-1248

7evam
  • 145
  • 3
  • 14
  • 1
    I have forked your codesandbox https://codesandbox.io/s/hungry-gagarin-il35g?file=/src/App.js, let me know if it worked for you. – Learner May 15 '20 at 18:32
  • @7evam Both `handleAddFile` and `changeLanguage` mutate state. This will prevent a re-render because react will not see the state has changed. – Brian Thompson May 15 '20 at 18:37
  • 1
    I have added the code update part and explanation in the answer – Learner May 15 '20 at 18:37

1 Answers1

5

The problem is in both methods handleAddFile and changeLanguage, you are directly accessing the state value files, so for updating you need to take the copy and do the changes and set the new values.

Updates you need to do in both methods

let newFiles = files;

To

let newFiles = [...files];

As @Brian Suggests, when you update deep nested values like language in the changeLanguage method. you can do something as shown below so it won't mutate the state.

const changeLanguage = (event, index) => {
  let newFiles = [...files];
  newFiles[index] = { ...newFiles[index], language: event.target.value };
  setFiles(newFiles);
};

Check this working codesandbox

Better explanation here

Learner
  • 8,379
  • 7
  • 44
  • 82
  • 1
    `changeLanguage` should also account for deeply nested state values. Even after correctly destructuring `files`, doing `newFiles[index].language = event.target.value;` will mutate the nested object (even though it probably won't prevent a re-render) – Brian Thompson May 15 '20 at 18:38
  • yes that works! I knew I couldn't directly mutate state but I see now that I unknowingly was. thanks for the help! – 7evam May 15 '20 at 18:40
  • [In this answer, under the Explanation heading](https://stackoverflow.com/questions/59738516/component-wont-update-when-a-redux-state-change-occurs/59738588#59738588) is a pretty detailed explanation of how to make true copies of objects (and arrays) in JS – Brian Thompson May 15 '20 at 18:41
  • @BrianThompson , I hope this is what you were expecting , correct me on this will add on the answer let newFiles = [...files]; let currentFile = newFiles[index]; const { value } = event.target currentFile = {...currentFile, language: value}; newFiles[index] = currentFile setFiles(newFiles); – Learner May 15 '20 at 18:43
  • It seems unnecessarily complex, but I think it works – Brian Thompson May 15 '20 at 18:47
  • as you suggested updated it, even i also initially thought but for better way of understanding you are free to update it. For future references it will be easy – Learner May 15 '20 at 18:49
  • cool and thanks added the link for explanation part you have. Happy coding :) #stay home #stay safe – Learner May 15 '20 at 18:56