Search code examples
reactjsreact-hooksreact-component

React Duplicate components updating wrong state: hooks


I'm a newbie to react, only been using it for a few days, so forgive me if this is a stupid question.

I have a file input component and an image thumbnail component, I use two duplicate file input components to update two different states then display the image from the different states in two different thumbnail components. I have unique keys set on all of the components, but only the state for the first component in the Dom is updated. When I add an image using the second file input, it updates the state belonging to the first file input.

I've tried looking for solutions and all of them state to use unique keys, which I think I have done properly.

let [certification, setCertification] = useState(null)
let [photoId, setPhotoId] = useState(null)

let handleUpdateCertificate = (e) =>{
    let file = e.target.files[0]
    console.log(file)
    let path = URL.createObjectURL(file)

    let newCertificate = {
        'file': file,
        'path' : path
    }

    setCertification(newCertificate)
}

let handleUpdatePhotoId = (e) => {
    let file = e.target.photoidinput.files[0]
    let path = URL.createObjectURL(file)

    let newPhotoID = {
        'file': file,
        'path' : path
    }

    setPhotoId(newPhotoID)

}

My return html is:

     <div className='justify-content-center margin-20' key='certificate-wrapper'>
        <ImgThumbnail key={'certificate'} name={'certificate'} image= 
             {certification?.path} wrapperClass={'justify-content-center margin-20'}/>
      </div>
      <div className='justify-content-center margin-20'>
         <FileInput key={'certificateinput'} name={'certificateinput'} labelText={<p 
                    className='text-paragraph edit-btn-text'>Add Certificate</p>} 
                     onChange={handleUpdateCertificate}
                     classWrapper={'edit-profile-responsive-btn-wrapper'}/>
      </div>
  <div className='justify-content-center margin-20 ' key='photo-Id'>
       <ImgThumbnail key={'photoid'} name={'photoId'} image={photoId?.path} 
                  wrapperClass={'justify-content-center margin-20'}/>
  </div>
                            
  <div className='justify-content-center margin-20' key='photo-id-input-wrapper'>
      <FileInput key={'photoidinput'} name={'photoidinput'} labelText={<p 
                  className='text-paragraph edit-btn-text'>Add Photo ID</p>} 
                  onChange={handleUpdatePhotoId}
                  classWrapper={'edit-profile-responsive-btn-wrapper'}/>
   </div>

Solution

  • Okay I'll give you some hints and then give you the working example:

    • You don't need to set key attribute if you are writing JSX elements like that, you need that only if you render a list of elements from an array, to prevent useless re-rendering when the array updates.

    • use const instead of let when a variable is static, there is a lint rule about it !

    • Try to use DRY, your update Handlers share a lot of logic, if you are going to add more inputs that would be all code repetition.

    Now the code:

    import React, { useState } from 'react';
    import './style.css';
    
    export default function App() {
      const [certification, setCertification] = useState(null);
      const [photoId, setPhotoId] = useState(null);
    
      const updateData = (file, cb) => {
        const path = URL.createObjectURL(file);
        const data = {
          file: file,
          path: path,
        };
        cb(data);
      };
    
      const handleUpdateCertificate = (e) => {
        updateData(e.target.files[0], setCertification);
      };
    
      const handleUpdatePhotoId = (e) => {
        updateData(e.target.files[0], setPhotoId);
      };
    
      return (
        <div>
          {certification && (
            <div className="justify-content-center margin-20">
              <ImgThumbnail
                name={'certificate'}
                image={certification?.path}
                wrapperClass={'justify-content-center margin-20'}
              />
            </div>
          )}
          <div className="justify-content-center margin-20">
            <FileInput
              id="certificate"
              name={'certificateinput'}
              labelText={
                <p className="text-paragraph edit-btn-text">Add Certificate</p>
              }
              onChange={handleUpdateCertificate}
              classWrapper={'edit-profile-responsive-btn-wrapper'}
            />
          </div>
          {photoId && (
            <div className="justify-content-center margin-20 " key="photo-Id">
              <ImgThumbnail
                name={'photoId'}
                image={photoId?.path}
                wrapperClass={'justify-content-center margin-20'}
              />
            </div>
          )}
    
          <div
            className="justify-content-center margin-20"
            key="photo-id-input-wrapper"
          >
            <FileInput
              id="photo"
              name={'photoidinput'}
              labelText={
                <p className="text-paragraph edit-btn-text">Add Photo ID</p>
              }
              onChange={handleUpdatePhotoId}
              classWrapper={'edit-profile-responsive-btn-wrapper'}
            />
          </div>
        </div>
      );
    }
    
    const FileInput = ({ id, labelText, ...props }) => (
      <label htmlFor={id}>
        {labelText}
        <input id={id} style={{ display: 'none' }} type="file" {...props} />
      </label>
    );
    
    const ImgThumbnail = ({ name, image }) => (
      <div>
        <img style={{ width: '100px', height: '100px' }} src={image} alt={name} />
      </div>
    );
    

    This example works right, you were probably doing something wrong inside FileInput Component, remember that a label has to have an htmlFor attribute with the id of the input element you want to trigger.

    Now, this code can be optimized and made more React style, since you might have more file inputs in the future, let's see how it can be optimized by creating reusable Components and compose them properly:

    import React, { useState } from 'react';
    import './style.css';
    
    /* INPUTS IMAGE TYPES */
    
    const inputs = [
      { type: 'photo', name: 'photo', label: 'Photo' },
      { type: 'certificate', name: 'certificate', label: 'Certificate' },
      { type: 'anotherType', name: 'anotherName', label: 'Another Input' },
    ];
    
    export default function App() {
      return (
        <div>
          {inputs.map((data) => (
            <ImagePreviewer key={data.type} data={data} />
          ))}
        </div>
      );
    }
    
    const FileInput = ({ id, labelText, ...props }) => (
      <label htmlFor={id}>
        {labelText}
        <input id={id} style={{ display: 'none' }} type="file" {...props} />
      </label>
    );
    
    const ImgThumbnail = ({ name, image }) => (
      <div>
        <img src={image} alt={name} />
      </div>
    );
    
    const ImagePreviewer = ({ data: { type, name, label } }) => {
      const [image, setImage] = useState(null);
    
      const updateData = (file, cb) => {
        const path = URL.createObjectURL(file);
        const data = {
          file: file,
          path: path,
        };
        cb(data);
      };
    
      const handleUpdate = (e) => {
        updateData(e.target.files[0], setImage);
      };
    
      return (
        <div>
          {image && (
            <div>
              <ImgThumbnail name={'name'} image={image?.path} />
            </div>
          )}
          <div>
            <FileInput
              id={name}
              name={name}
              labelText={<p>Add {label}</p>}
              onChange={handleUpdate}
            />
          </div>
        </div>
      );
    };
    

    A working demo HERE.