Search code examples
reactjsreact-hooksfrontendreact-forwardref

How to get acces to a child useState in React?


I'm trying to done a form in react that have subcomponents for uploaded images (to do a preview and do it more beautyfull) but the thing is that I can't access to the useState of the child where is the image that I need to send to de backend.

Here is the code of the subcomponent and in the useState I need to acces throught the parent to the image:

import React, { useState, Fragment } from "react";
import {
  Layout,
  Container,
  BoxUpload,
  ContainerUploadImage,
  TextUploadImage,
  LabelUploadImage,
  ImagePreview,
} from "./ImageUploadElements";
import UploadPhoto from "../../../images/upload.svg";
import CloseIcon from "../../../images/close.svg";

const ImageUpload = ({text}) => {
  const [image, setImage] = useState("");
  const [isUploaded, setIsUploaded] = useState(false);

  const handleImageChange = (e) => {
    if (e.target.files && e.target.files[0]) {
      let reader = new FileReader();

      reader.onload = (e) => {
        setImage(e.target.result);
        setIsUploaded(true);
      };
      reader.readAsDataURL(e.target.files[0]);
    }
  };

  return (
    <Layout>
      <Container>
        <h2>{text}</h2>
        <BoxUpload>
          <div className="image-upload">
            {isUploaded ? (
              <ImagePreview>
                <img
                  className="close-icon"
                  src={CloseIcon}
                  alt="CloseIcon"
                  onClick={() => {
                    setIsUploaded(false);
                    setImage(null);
                  }}
                />
                <img
                  src={image}
                  className="uploaded-image"
                  draggable={false}
                  alt="progress-uploaded"
                />
              </ImagePreview>
            ) : (
              <Fragment>
                <LabelUploadImage htmlFor="upload-input">
                  <ContainerUploadImage
                    src={UploadPhoto}
                    alt="Upload Icon"
                    draggable={false}
                  />
                  <TextUploadImage>Click to upload image</TextUploadImage>
                </LabelUploadImage>
                <input
                  type="file"
                  name="upload-input"
                  accept=".jpg,.jpeg,.gif,.png,.mov,.mp4"
                  onChange={handleImageChange}
                />
              </Fragment>
            )}
          </div>
        </BoxUpload>
      </Container>
    </Layout>
  );
};

export default ImageUpload;

And here in that upload form component is where I need to get acces to this image to send it with axios to backend:

import React, { Fragment, useState } from "react";
import {
  Container,
  FormWrap,
  FormContent,
  Form,
  FormH1,
  FormLabel,
  FormInput,
  FormButton,
  FormErrorWrap,
  FormError,
  FormErrorText,
  PhotoWrap
} from "./UploadElements";
import ImageUpload from "../ImageUpload";
import { frontPhotoText, sidePhotoText, backPhotoText } from "./Data";

const Upload = () => {
  const [weight, setWeight] = useState("");
  

  const [uploadErrors, setUploadErrors] = useState([{}]);

  const upload  = (e) => {
    e.preventDefault();
    // Here will go the axios petición with the wight and the three images uploaded.
  }

  return (
    <Fragment>
      <Container>
        <FormWrap>
          <FormContent>
            <Form onSubmit={upload}>
              <FormH1>Upload New Progress</FormH1>
              <FormLabel htmlFor="weight">Weight</FormLabel>
              <FormInput
                onChange={(e) => setWeight(e.target.value)}
                type="number"
                value={weight}
                id="weight"
                required
              />
              <PhotoWrap>
                <ImageUpload {...frontPhotoText}/>
                <ImageUpload {...sidePhotoText}/>
                <ImageUpload {...backPhotoText}/>
              </PhotoWrap>
              <FormErrorWrap>
                {uploadErrors ? (
                  uploadErrors.map((err, index) => (
                    <FormError key={index}>
                      <FormErrorText>{err.msg}</FormErrorText>
                    </FormError>
                  ))
                ) : (
                  <Fragment></Fragment>
                )}
              </FormErrorWrap>
              <FormButton>Upload</FormButton>
            </Form>
          </FormContent>
        </FormWrap>
      </Container>
    </Fragment>
  );
};

export default Upload;

But I don't know how can I get this images throught the parent, if anyone can help I'll be very gratefull, thanks!!!


Solution

  • You can use a combination of forwardRef and useImperativeHandle to expose out a function from the child component that a parent component can invoke.

    Child - Import and decorate the child component with forwardRef and use the useImperativeHandle to expose a getImage function that returns the current image state.

    import React, { useState, Fragment, forwardRef } from "react";
    ...
    
    const ImageUpload = forwardRef(({text}, ref) => {
      const [image, setImage] = useState("");
      const [isUploaded, setIsUploaded] = useState(false);
    
      useImperativeHandle(ref, () => ({
        getImage: () => image,
      }));
    
      const handleImageChange = (e) => {
        ...
      };
    
      return (
        ...
      );
    });
    

    Parent - Create a React ref to pass to ImageUpload and in the callback access the current ref value and invoke the function.

    import React, { Fragment, useState, useRef } from "react";
    ...
    
    const Upload = () => {
      const [weight, setWeight] = useState("");
    
      const imageUploadFrontRef = useRef();
      const imageUploadSideRef = useRef();
      const imageUploadBackRef = useRef();
    
      const [uploadErrors, setUploadErrors] = useState([{}]);
    
      const upload  = (e) => {
        e.preventDefault();
        
        const imageFront = imageUploadFrontRef.current.getImage();
        const imageSide = imageUploadSideRef.current.getImage();
        const imageBack = imageUploadBackRef.current.getImage();
    
        // do with the images what you need.
      }
    
      return (
        <Fragment>
          <Container>
            <FormWrap>
              <FormContent>
                <Form onSubmit={upload}>
                  ...
                  <PhotoWrap>
                    <ImageUpload ref={imageUploadFrontRef} {...frontPhotoText} />
                    <ImageUpload ref={imageUploadSideRef} {...sidePhotoText} />
                    <ImageUpload ref={imageUploadBackRef} {...backPhotoText} />
                  </PhotoWrap>
                  ...
                </Form>
              </FormContent>
            </FormWrap>
          </Container>
        </Fragment>
      );
    };