ReactJS: How to handle Image / File upload with Formik?
Asked Answered
W

7

62

I am designing a profile page for my site using ReactJS. Now my question is how do I upload the image from local machine and save it to the database and also displaying it in the profile page

import React, {Component} from 'react';
import { connect } from 'react-redux';
import { AccountAction } from '../../actions/user/AccountPg1Action';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

class AccountInfo extends Component {
  constructor(props) {
    super(props) 
    this.state = {
      currentStep: 1,
      userAccountData: {
        userid: '',
        useravtar: '',
        attachement_id: '',
   }
  }
 }

handleFileUpload = (event) => {
  this.setState({useravtar: event.currentTarget.files[0]})
};

handleChange = event => {
    const {name, value} = event.target
    this.setState({
      [name]: value
    })    
  }

handleSubmit = event => {
    let that = this;
    const { AccountAction } = that.props;
    event.preventDefault();

    let accountInputs = {
      userid: 49,
      useravtar: that.state.image,
      attachement_id: 478,
}
    that.setState({
      userAccountData: accountInputs,
    })

    AccountAction(accountInputs)
  }
AccountInfoView = () => {
console.log(this.state.useravtar)
    return (
      <section id="account_sec" className="second_form">
      <div className="container">
      <React.Fragment>
        <Formik
          initialValues={‌{
            file: null,
            email: '',
            phone: ''
          }}
          validationSchema={accountInfoSchema}
          render={(values) => {
          return(
        <Form onSubmit={this.handleSubmit}>
        <Step1 
          currentStep={this.state.currentStep} 
          handleChange={this.handleChange}
          file= {this.state.useravtar}
          handleFileUpload={this.handleFileUpload}
          />
          </Form>
        );
      }}
      />
      </React.Fragment>
      )
  }

  render() {    

    return (
      <div>{this.authView()}</div>
    )
  }
}

function Step1(props) {
console.log(props.useravtar)
  if (props.currentStep !== 1) {
    return null
  } 

  return(
    <div className="upload">
        <label htmlFor="profile">
          <div className="imgbox">
            <img src="images/trans_116X116.png" alt="" />
            <img src={props.useravtar} className="absoImg" alt="" />
          </div>
        </label>
<input id="file" name="file" type="file" accept="image/*" onChange={props.handleFileUpload}/>
        <span className="guide_leb">Add your avatar</span>
      </div>
  )
}

When I do console in handleChange action for event.target.file[0] it responds with undefined.

Also, doing a console.log(this.state.useravtar) in handleSubmit action it shows a pathname like c:/fakepath/imgname.jpg

P.S: I have a multiple forms so I am using it in a Step wise. And i am using Redux Reducer for storing the data.

I have referred this link but my requirement is not looking like this.

Whereto answered 15/5, 2019 at 12:49 Comment(3)
have you tried using a form data object - developer.mozilla.org/en-US/docs/Web/API/FormData/…Zeidman
@SumanthMadishetty : i have used a Formik library so i have used a Field component from this library. jaredpalmer.com/formikWhereto
@Taalavya formik doesnot have file upload component, you have to use html input and use ` setFieldValue` method of formik to set the dataBattat
B
87

Formik doesnot support fileupload by default, But you can try the following

<input id="file" name="file" type="file" onChange={(event) => {
  setFieldValue("file", event.currentTarget.files[0]);
}} />

Here "file" represents the key that you are using for holding the file

And on submit you can get the filename, size etc for the file by using

onSubmit={(values) => {
        console.log({ 
              fileName: values.file.name, 
              type: values.file.type,
              size: `${values.file.size} bytes`
            })

If you want to set the file into components state then you can use

onChange={(event) => {
  this.setState({"file": event.currentTarget.files[0]})};
}}

According to your code, you have to handle file upload as below

In AccountInfo add a function to handle file upload

handleFileUpload = (event) => {
this.setState({WAHTEVETKEYYOUNEED: event.currentTarget.files[0]})};
}

And pass the same function to Step1 Component as below

    <Step1 
      currentStep={this.state.currentStep} 
      handleChange={this.handleChange}
      file= {this.state.image}
      handleFileUpload={this.handleFileUpload}
      />

In Step1 Component where you upload the file, Change the input as

<input id="file" name="file" type="file" accept="image/*" onChange={props.handleFileUpload}/>

If you need to preview the uploaded image then you can create a blob and pass the same as source for image as below

<img src={URL.createObjectURL(FILE_OBJECT)} /> 

EDIT-1

As URL.createObjectURL method is deprecated due to security issues, we need to use srcObject for Media Elements, to use that you can use ref to assign srcObject, for example

Assuming you are using class Components,

Constructor

in constructor you can use

constructor(props) {
  super(props)
  this.imageElRef = React.createRef(null)
}

HANDLE CHANGE FUNCTION

handleFileUpload = (event) => {
  let reader = new FileReader();
let file = event.target.files[0];
reader.onloadend = () => {
  this.setState({
    file: reader.result
  });
};
reader.readAsDataURL(file);
}

Element

<img src={this.state.file} /> 
Battat answered 16/5, 2019 at 4:50 Comment(15)
Where to define this setFieldValue, it throws me an error of undefined like: ./src/components/user/AccountInfo.jsx Line 266: 'setFieldValue' is not defined no-undefWhereto
setFieldValue is obtained from <Formik />, refer: jaredpalmer.com/formik/docs/api/…Titanomachy
@Taalavya if you want to store the file directly to state you can use the method that i mentionedBattat
@SumanthMadishetty yess correct but it throws this error from a function Step1(props){...} function. prnt.sc/np7gnpWhereto
What do u mean by using props.setState...can you update the code with implementationBattat
@Taalavya Updated my code,According to your implementation, Please checkBattat
yess now i am getting some result. So, doing console.log(this.state.useravtar) i am getting this result. prnt.sc/np84z8 But how can i get a path of uploaded image in for preview ? I did something like this : <img src={props.useravtar} className="absoImg" alt="" /> <input id="file" name="file" type="file" accept="image/*" onChange={props.handleFileUpload}/> so this props.useravtar gives an undefined value for preview image. handleFileUpload = (event) => { this.setState({useravtar: event.currentTarget.files[0]}) };Whereto
@SumanthMadishetty: can you please update your answer that will be help to other developers also in future.Whereto
Interesting. I'm getting an error when I used setFieldValue() the way you suggest, @muthanth. This is the error: Uncaught DOMException: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.Costanza
createObjectURL is now deprecated in most of the browsers. I researched it and found that instead of src, we need to use srcObject. But I don't know how to use in <img /> tag. Anyone out there and @SumanthMadishetty Please look into it. It will very helpful for me a lot.Rattlepate
Thanks, @SumanthMadishetty for the edit. I'm facing an issue which is when I upload the image, my modal refreshes and the file input field lost the value.Rattlepate
Is there a useFormik example? because I don't think useFormik has a setFieldValue method?Dandy
I get InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable in Firefox. Need to set the input's value to an empty string (based on the error message I got in Chrome) to avoid the error.Dieppe
facing this error ==> Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.Rent
Yes, as @HassanAliShahzad pointed out, the input value has to be a string. It's better to setFieldValue('file', e.target.value) and manage a separate state variable with the file itself - setFile(reader.result).Gallium
S
13

Here is how I resolved it with Formik and Material UI

in your JS file, just declare a variable avatarPreview like below

  const [avatarPreview, setAvatarPreview] = useState('/avatars/default.png');



           <Box
            display='flex'
            textAlign='center'
            justifyContent='center'
            flexDirection='column'>
           
            <ImageAvatar size='md' src={avatarPreview || user?.avatar} />

            <Button
              variant='contained'
              component='label'
              startIcon={<CloudUploadIcon />}>
              Choose Avatar
              <input
                name='avatar'
                accept='image/*'
                id='contained-button-file'
                type='file'
                hidden
                onChange={(e) => {
                  const fileReader = new FileReader();
                  fileReader.onload = () => {
                    if (fileReader.readyState === 2) {
                      setFieldValue('avatar', fileReader.result);
                      setAvatarPreview(fileReader.result);
                    }
                  };
                  fileReader.readAsDataURL(e.target.files[0]);
                }}
              />
            </Button>
          </Box>

Default Preview: Default avatar upload

After choosing avatar: After you choose your avatar

Swingeing answered 18/8, 2021 at 21:51 Comment(1)
Setting your avatar as the Avatar. haPostpone
A
4

You can upload single or multiple files with validation using Formik as follows:

import "./App.css";
import { useEffect, useState } from "react";
import * as Yup from "yup";
import { Formik, Field, Form, ErrorMessage, useField } from "formik";
import axios from "axios";


function App() {
  return (
    <Formik
      initialValues={{
        profile: [],
      }}
      validationSchema={Yup.object({
        profile:Yup.array().min(1,"select at least 1 file")
      })}
      onSubmit={(values, props) => {
        let data = new FormData();
        values.profile.forEach((photo, index) => {
          data.append(`photo${index}`, values.profile[index]);
        });
        axios
          .post("you_api_for_file_upload", data, {
            headers: {
              "Content-Type": "multipart/form-data",
            },
          })
          .then((response) => {
            console.log(response);
          })
          .catch((err) => {
            console.log(err);
          });
      }}
    >
      {(formik) => {
        return (
          <>
            <Form>
              <input
                id="file"
                name="profile"
                type="file"
                onChange={(event) => {
                  const files = event.target.files;
                  let myFiles =Array.from(files);
                  formik.setFieldValue("profile", myFiles);
                }}
                multiple
              />
              <ErrorMessage name="profile"/>
              <button type="submit" disabled={formik.isSubmitting}>
                Submit
              </button>
            </Form>
          </>
        );
      }}
    </Formik>
  );
}

export default App;

Note: you can customize min(your choice, "your message") as per your need.

   validationSchema={Yup.object({
        profile:Yup.array().min(1,"select at least 1 file")
      })}
Amend answered 2/8, 2021 at 13:10 Comment(1)
You cannot use input name and setFieldValue as same value, you could use only if it is of string type. For types other than string the setFieldValue must be of different keyDanidania
I
4

FormIk does not support file uploading and we need to do it in custom way. Just trigger onChange event and set the file.

If you face any error of setFieldValue in TypeScript, then you simply do this:

onChange={(event) => {
   if (event.currentTarget.files) {
       formik.setFieldValue(
           "file",
            event.currentTarget.files[0]
        );
   }
Idle answered 13/6, 2022 at 12:21 Comment(0)
S
2

To handle file in formik with backend all you need to do add the input given below (you can change avatar to anything you want):

<input
 type="file"
 name="avatar"
 onChange={(event) => {
 setFieldValue('avatar', event.currentTarget.files[0]);
 }}
 />

onSubmit you can access file using values obj like values.avatar.

On server-side (express) to access the file we use req.file You also have to use multer that will automatically detect file to handle it on the server-side

Shelter answered 1/2, 2022 at 14:5 Comment(1)
So firstly we need inside onSubmit, make a request with mutipart/form-data to upload all files and then second request to create a new entity (and for example, we can receive URL or FileEntity from the first upload request and pass it to the create dto)?Commingle
D
1

I used simple trick

<Field name="image">
{ (form , meta , value ) =>
const {setFieldValue} = form 
return (  
<input name="image" type="file" onChange={(e) => setFieldValue(e.target.file[0])}
)
}
</Field>

for multiple image
<input name="image" type="file" onChange={(e) => setFieldValue(e.target.files)}
or use loop
for( i=0 ; i < e.target.file; i++){
setFieldValue([...image , e.target.file])
}
Dither answered 11/3, 2022 at 9:56 Comment(0)
S
0

To handle file in formik with backend all you need to do add the input given below (you can change avatar to anything you want):

<input
 type="file"
 name="avatar"
 onChange={(event) => {
 setFieldValue('avatar', event.currentTarget.files[0]);
 }}
 />

onSubmit you can access file using values obj like values.avatar.

On server-side (express) to access the file we use req.file You also have to use multer & cloudinary that will detect file to handle it on the server-side

Shelter answered 1/2, 2022 at 14:8 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.