How to upload file with redux-form?
Asked Answered
D

8

29

I can't get correct value into the store when trying to upload a file. Instead of file content, I get something like { 0: {} }. Here's the code:

const renderInput = field => (
  <div>
    <input {...field.input} type={field.type}/>
    {
      field.meta.touched &&
      field.meta.error &&
      <span className={styles.error}>{field.meta.error}</span>
    }
  </div>
);

render() {

  ...

  <form className={styles.form} onSubmit={handleSubmit(submit)}>
    <div className={styles.interface}>
      <label>userpic</label>
      <Field
        name="userpic"
        component={renderInput}
        type="file"
      />
    </div>
    <div>
      <button type="submit" disabled={submitting}>Submit</button>
    <div>
  </form>

  ...

}

All the examples on the web that I found were made using v5 of redux-form.

How do I do file input in redux-form v6?

Duren answered 26/9, 2016 at 8:45 Comment(0)
L
30

Create a Field Component like:

import React, {Component} from 'react'

export default class FieldFileInput  extends Component{
  constructor(props) {
    super(props)
    this.onChange = this.onChange.bind(this)
  }

  onChange(e) {
    const { input: { onChange } } = this.props
    onChange(e.target.files[0])
  }

  render(){
    const { input: { value } } = this.props
    const {input,label, required, meta, } = this.props  //whatever props you send to the component from redux-form Field
    return(
     <div><label>{label}</label>
     <div>
       <input
        type='file'
        accept='.jpg, .png, .jpeg'
        onChange={this.onChange}
       />
     </div>
     </div>
    )
}
}

Pass this component to the Field component where you needed. No need of additional Dropzone or other libraries if you are after a simple file upload functionality.

Langur answered 26/10, 2017 at 6:16 Comment(6)
Your answer should be the accepted answer since it's the simpler way to make it work with redux-form without any other dependency. :+1:Faradmeter
@OwlyMoly Uncaught TypeError: Cannot read property 'onChange' of undefinedChangeup
@Changeup : It's weird, this is working for me. Have you tried passing the handle change method to this Field as onChange prop for the field. Example:` <Field name="fileUpload" label="Upload New Screenshot" type="file" component={FieldFileInput} onChange={handleChange} />`Langur
@Changeup Trigger change with this handleChange to handle field value change on every time you change the file uploaded. I hope this should fix your problem.Langur
Doing this is giving me an error in the console A non-serializable value was detected in an action, in the path: payload. Value:. It's working but prints that out, any ideas?Colettecoleus
@Colettecoleus Is that an error or warning? I think it should be a warning raised by the redux store. The path is not a serialized data when saved to the store in your case. check the serialised type of value it might be expecting then De-serialise the input to a string/whatever is expected. Easy way is to try it out in the onChange handler method.Langur
T
13

My example of redux form input wrapper with Dropzone

import React, {Component, PropTypes} from 'react';
import Dropzone from 'react-dropzone';
import { Form } from 'elements';
import { Field } from 'redux-form';

class FileInput extends Component {
  static propTypes = {
    dropzone_options: PropTypes.object,
    meta: PropTypes.object,
    label: PropTypes.string,
    classNameLabel: PropTypes.string,
    input: PropTypes.object,
    className: PropTypes.string,
    children: PropTypes.node,
    cbFunction: PropTypes.func,
  };

  static defaultProps = {
    className: '',
    cbFunction: () => {},
  };

  render() {
    const { className, input: { onChange }, dropzone_options, meta: { error, touched }, label, classNameLabel, children, name, cbFunction } = this.props;

    return (
      <div className={`${className}` + (error && touched ? ' has-error ' : '')}>
        {label && <p className={classNameLabel || ''}>{label}</p>}
        <Dropzone
          {...dropzone_options}
          onDrop={(f) => {
            cbFunction(f);
            return onChange(f);
          }}
          className="dropzone-input"
          name={name}
        >
          {children}
        </Dropzone>
        {error && touched ? error : ''}
      </div>
    );
  }
}
export default props => <Field {...props} component={FileInput} />;

Hot to use it:

<FileInput
 name="add_photo"
 label="Others:"
  classNameLabel="file-input-label"
  className="file-input"
  dropzone_options={{
    multiple: false,
    accept: 'image/*'
  }}
>
  <span>Add more</span>
</FileInput>
Togoland answered 6/10, 2016 at 8:20 Comment(1)
I believe that the file received in the onDrop function needs to be read with a FileReader before it can be send to a server. Else it's usless. If it helps completely answering the original question I could place an example here.Hexapla
D
10

Another way to do it that will render a preview image (the below example uses React 16+ syntax and only accepts a single image file to send to an API; however, with some minor tweaks, it can also scale to multiple images and other fields inputs):

Working example: https://codesandbox.io/s/m58q8l054x

Working example (outdated): https://codesandbox.io/s/8kywn8q9xl


Before:

enter image description here

After:

enter image description here


containers/UploadForm.js

import React, { Component } from "react";
import { Form, Field, reduxForm } from "redux-form";
import DropZoneField from "../components/dropzoneField";

const imageIsRequired = value => (!value ? "Required" : undefined);

class UploadImageForm extends Component {
  state = { imageFile: [] };

  handleFormSubmit = formProps => {
    const fd = new FormData();
    fd.append("imageFile", formProps.imageToUpload.file);
    // append any additional Redux form fields
    // create an AJAX request here with the created formData

    alert(JSON.stringify(formProps, null, 4));
  };

  handleOnDrop = (newImageFile, onChange) => {
    const imageFile = {
      file: newImageFile[0],
      name: newImageFile[0].name,
      preview: URL.createObjectURL(newImageFile[0]),
      size: newImageFile[0].size
    };

    this.setState({ imageFile: [imageFile] }, () => onChange(imageFile));
  };

  resetForm = () => this.setState({ imageFile: [] }, () => this.props.reset());

  render = () => (
    <div className="app-container">
      <h1 className="title">Upload An Image</h1>
      <hr />
      <Form onSubmit={this.props.handleSubmit(this.handleFormSubmit)}>
        <Field
          name="imageToUpload"
          component={DropZoneField}
          type="file"
          imagefile={this.state.imageFile}
          handleOnDrop={this.handleOnDrop}
          validate={[imageIsRequired]}
        />
        <button
          type="submit"
          className="uk-button uk-button-primary uk-button-large"
          disabled={this.props.submitting}
        >
          Submit
        </button>
        <button
          type="button"
          className="uk-button uk-button-default uk-button-large"
          disabled={this.props.pristine || this.props.submitting}
          onClick={this.resetForm}
          style={{ float: "right" }}
        >
          Clear
        </button>
      </Form>
      <div className="clear" />
    </div>
  );
}

export default reduxForm({ form: "UploadImageForm" })(UploadImageForm);

components/dropzoneField.js

import React from "react";
import PropTypes from "prop-types";
import DropZone from "react-dropzone";
import ImagePreview from "./imagePreview";
import Placeholder from "./placeholder";
import ShowError from "./showError";

const DropZoneField = ({
  handleOnDrop,
  input: { onChange },
  imagefile,
  meta: { error, touched }
}) => (
  <div className="preview-container">
    <DropZone
      accept="image/jpeg, image/png, image/gif, image/bmp"
      className="upload-container"
      onDrop={file => handleOnDrop(file, onChange)}
    >
      {({ getRootProps, getInputProps }) =>
        imagefile && imagefile.length > 0 ? (
          <ImagePreview imagefile={imagefile} />
        ) : (
          <Placeholder
            error={error}
            touched={touched}
            getInputProps={getInputProps}
            getRootProps={getRootProps}
          />
        )
      }
    </DropZone>
    <ShowError error={error} touched={touched} />
  </div>
);

DropZoneField.propTypes = {
  error: PropTypes.string,
  handleOnDrop: PropTypes.func.isRequired,
  imagefile: PropTypes.arrayOf(
    PropTypes.shape({
      file: PropTypes.file,
      name: PropTypes.string,
      preview: PropTypes.string,
      size: PropTypes.number
    })
  ),
  label: PropTypes.string,
  onChange: PropTypes.func,
  touched: PropTypes.bool
};

export default DropZoneField;

components/imagePreview.js

import React from "react";
import PropTypes from "prop-types";

const ImagePreview = ({ imagefile }) =>
  imagefile.map(({ name, preview, size }) => (
    <div key={name} className="render-preview">
      <div className="image-container">
        <img src={preview} alt={name} />
      </div>
      <div className="details">
        {name} - {(size / 1024000).toFixed(2)}MB
      </div>
    </div>
  ));

ImagePreview.propTypes = {
  imagefile: PropTypes.arrayOf(
    PropTypes.shape({
      file: PropTypes.file,
      name: PropTypes.string,
      preview: PropTypes.string,
      size: PropTypes.number
    })
  )
};

export default ImagePreview;

components/placeholder.js

import React from "react";
import PropTypes from "prop-types";
import { MdCloudUpload } from "react-icons/md";

const Placeholder = ({ getInputProps, getRootProps, error, touched }) => (
  <div
    {...getRootProps()}
    className={`placeholder-preview ${error && touched ? "has-error" : ""}`}
  >
    <input {...getInputProps()} />
    <MdCloudUpload style={{ fontSize: 100, paddingTop: 85 }} />
    <p>Click or drag image file to this area to upload.</p>
  </div>
);

Placeholder.propTypes = {
  error: PropTypes.string,
  getInputProps: PropTypes.func.isRequired,
  getRootProps: PropTypes.func.isRequired,
  touched: PropTypes.bool
};

export default Placeholder;

components/showError.js

import React from "react";
import PropTypes from "prop-types";
import { MdInfoOutline } from "react-icons/md";

const ShowError = ({ error, touched }) =>
  touched && error ? (
    <div className="error">
      <MdInfoOutline
        style={{ position: "relative", top: -2, marginRight: 2 }}
      />
      {error}
    </div>
  ) : null;

ShowError.propTypes = {
  error: PropTypes.string,
  touched: PropTypes.bool
};

export default ShowError;

styles.css

img {
  max-height: 240px;
  margin: 0 auto;
}

.app-container {
  width: 500px;
  margin: 30px auto;
}

.clear {
  clear: both;
}

.details,
.title {
  text-align: center;
}

.error {
  margin-top: 4px;
  color: red;
}

.has-error {
  border: 1px dotted red;
}

.image-container {
  align-items: center;
  display: flex;
  width: 85%;
  height: 80%;
  float: left;
  margin: 15px 10px 10px 37px;
  text-align: center;
}

.preview-container {
  height: 335px;
  width: 100%;
  margin-bottom: 40px;
}

.placeholder-preview,
.render-preview {
  text-align: center;
  background-color: #efebeb;
  height: 100%;
  width: 100%;
  border-radius: 5px;
}

.upload-container {
  cursor: pointer;
  height: 300px;
}
Drillstock answered 25/10, 2017 at 4:32 Comment(2)
Thanks for the code, this will not work with the latest version of react-dropzone though. Be sure to use "react-dropzone": "5.0.1" in your dependencies.Stuffy
@Stuffy -- Updated all dependencies to latest and included new code.Drillstock
N
6

I managed to do it with redux-form on material-ui wrapping TextField like this:

B4 edit:

befor edit

After edit:

after edit

 <Field name="image" component={FileTextField} floatingLabelText={messages.chooseImage} fullWidth={true} />

with component defined as:

const styles = {
  button: {
    margin: 12
  },
  exampleImageInput: {
    cursor: 'pointer',
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    width: '100%',
    opacity: 0
  },
  FFS:{
    position: 'absolute',
    lineHeight: '1.5',
    top: '38',
    transition: 'none',
    zIndex: '1',
    transform: 'none',
    transformOrigin: 'none',
    pointerEvents: 'none',
    userSelect: 'none',
    fontSize: '16',
    color: 'rgba(0, 0, 0, 0.8)',
  }
};

export const FileTextField  = ({
                                  floatingLabelText,
                                  fullWidth,
                                  input,
                                  label,
                                  meta: { touched, error },
                                  ...custom })=>{
  if (input.value && input.value[0] && input.value[0].name) {
    floatingLabelText = input.value[0].name;
  }
  delete input.value;
  return (
    <TextField
      hintText={label}
      fullWidth={fullWidth}
      floatingLabelShrinkStyle={styles.FFS}
      floatingLabelText={floatingLabelText}
      inputStyle={styles.exampleImageInput}
      type="file"
      errorText={error}
      {...input}
      {...custom}
    />
  )
}
Nullity answered 9/6, 2018 at 20:16 Comment(0)
H
4

If you need base64 encoding to send it to your backend, here is a modified version that worked for me:

export class FileInput extends React.Component {

  getBase64 = (file) => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result);
      reader.onerror = error => reject(error);
    });
  }

  onFileChange = async (e) => {
    const { input } = this.props
    const targetFile = e.target.files[0]
    if (targetFile) {
      const val = await this.getBase64(targetFile)
      input.onChange(val)
    } else {
      input.onChange(null)
    }
  }

  render() {

    return (
      <input
        type="file"
        onChange={this.onFileChange}
      />
    )
  }
}

Then your field component would look like:

<Field component={FileInput} name="primary_image" type="file" />

Holden answered 31/5, 2018 at 19:15 Comment(1)
How do remove blob file link example Data:application/pdf;base64, docBlob: "data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9Qcm9kdWNlcihEeW5hbWljUERGIGZvciAuTkVUIHY4LjAuMS40MCBcKETroublesome
T
3

For React >= 16 and ReduxForm >= 8 (tested version are 16.8.6 for React and 8.2.5) works following component.

(Solution posted in related GitHub issue by DarkBitz)

const adaptFileEventToValue = delegate => e => delegate(e.target.files[0]);

const FileInput = ({ 
  input: { value: omitValue, onChange, onBlur, ...inputProps }, 
  meta: omitMeta, 
  ...props 
}) => {
  return (
    <input
      onChange={adaptFileEventToValue(onChange)}
      onBlur={adaptFileEventToValue(onBlur)}
      type="file"
      {...props.input}
      {...props}
    />
  );
};

export const FileUpload = (props) => {
    const { handleSubmit } = props;
    const onFormSubmit = (data) => {
        console.log(data);
    }
    return (
          <form onSubmit={handleSubmit(onFormSubmit)}>
            <div>
              <label>Attachment</label>
              <Field name="attachment" component={FileInput} type="file"/>
            </div>
            <button type="submit">Submit</button>
          </form>
    )
}
Terrific answered 28/8, 2019 at 9:12 Comment(1)
I get this when I'm using your solution ``` Error: Invariant failed: A state mutation was detected between dispatches, in the path 'form.UploadImageForm.values.attachment.lastModifiedDate'.```Crusade
L
2

With Redux Form

const { handleSubmit } = props;
  //make a const file to hold the file prop.
  const file = useRef();
  
  // create a function to replace the redux-form input-file value to custom value.
  const fileUpload = () => {
  // jsx to take file input
  // on change store the files /file[0] to file variable
    return (
      <div className='file-upload'>
        <input
          type='file'
          id='file-input'
          accept='.png'
          onChange={(ev) => {
            file.current = ev.target.files;
          }}
          required
        />
      </div>
    );
  };

  //catch the redux-form values!
  //loop through the files and add into formdata
  //form data takes key and value
  //enter the key name as multer-config fieldname
  //then add remaining data into the formdata
  //make a request and send data.
  const onSubmitFormValues = (formValues) => {
    const data = new FormData();
    for (let i = 0; i < file.current.length; i++) {
      data.append("categoryImage", file.current[i]);
    }

    data.append("categoryName", formValues.categoryName);
  Axios.post("http://localhost:8080/api/v1/dev/addNewCategory", data)
      .then((response) => console.log(response))
      .catch((err) => console.log(err));
  };
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Leola answered 25/8, 2020 at 11:11 Comment(0)
D
0

You can also use react-dropzone for this purpose. The below code worked fine for me

filecomponent.js

import React from 'react'
import { useDropzone } from 'react-dropzone'
function MyDropzone(props) {

    const onDrop = (filesToUpload) => {
        return props.input.onChange(filesToUpload[0]);
    }

    const onChange = (filesToUpload) => {
        return props.input.onChange(filesToUpload[0]);
    }

    const { getRootProps, getInputProps } = useDropzone({ onDrop });
    return (
         <div {...getRootProps()}>
          <input {...getInputProps()} onChange={e => onChange(e.target.files)} />
          <p> Drop or select yout file</p>
         </div>
        )
}

export default MyDropzone;

In form use this

     <Field
       name="myfile"
       component={renderFile}
     />
Debra answered 27/4, 2021 at 7:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.