Search code examples
javascriptnode.jsexpressreduxmulter

How to pass an image to the backend from react and redux


I'm using the MERN stack and I want to be able to upload an image in the front end (react) and access it in the backend (express, nodejs) to store it later. I'm using multer but I keep getting undefinied when I try to console.log() the req.file object.

frontend:

import React, { Component } from "react";
import { Link, withRouter } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { addPlayer } from "../../actions/profileActions";

class AddPlayer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      avatarFile: null,
      name: "",
      age: "",
      apodo: "",
      captain: false,
      description: "",
      atributes: "",
      facebook: "",
      instagram: "",
      twitter: "",
      youtube: "",
    };
    this.onSubmit = this.onSubmit.bind(this);
    this.onImageChange = this.onImageChange.bind(this);
  }

  onSubmit(e) {
    e.preventDefault();

    const playerData = {
      name: this.state.name,
      age: this.state.age,
      apodo: this.state.apodo,
      captain: this.state.captain,
      description: this.state.description,
      atributes: this.state.atributes,
      facebook: this.state.facebook,
      instagram: this.state.instagram,
      twitter: this.state.twitter,
      youtube: this.state.youtube
    };

    this.props.addPlayer(playerData, this.props.history);
  }

  onImageChange(event) {
    if (event.target.files && event.target.files[0]) {
      this.setState({ avatarFile: event.target.files[0] });
    }
  }

  render() {
    return(
      <div>
        <form
          onSubmit={this.onSubmit}
          method="POST"
          encType="multipart/form-data"
        >
          <div className="text-center mb-3">
            <input
              type="file"
              name="file"
              id="file"
              accept="image/*"
              className="inputfile"
              onChange={this.onImageChange}
             />
             <label htmlFor="file" className="btn btn-primary">
               Elegir foto
             </label>
            </div>
        </form>
      </div>
    );
  }

}

AddPlayer.propTypes = {
  addPlayer: PropTypes.func.isRequired,
  profile: PropTypes.object.isRequired,
  errors: PropTypes.object.isRequired
};

const mapStateToProps = state => ({
  profile: state.profile,
  errors: state.errors
});

export default connect(
  mapStateToProps,
  { addPlayer }
)(withRouter(AddPlayer));

The AddPlayer action

//Create player
export const addPlayer = (playerData, history) => dispatch => {
  axios
    .post("api/profile/player", playerData)
    .then(res => history.push("/dashboard"))
    .catch(err =>
      dispatch({
        type: GET_ERRORS,
        payload: err.response.data
      })
    );
};

it just post the player data and then redirect to another component.

the backend is divided in two files. server.js where all the middlewares are set up and profile.js that contain all the routes.

backend server.js

const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const passport = require("passport");
const cors = require("cors");

const users = require("./routes/api/users");
const profile = require("./routes/api/profile");
const matches = require("./routes/api/matches");

const app = express();

//body-parser middleware
app.use(bodyParser.urlencoded({ extended: false, limit: "50mb" }));
app.use(bodyParser.json({ limit: "50mb" }));

//db config
const db = require("./config/keys").mongoURI;

//cors
app.use(cors());

//connect to mongoose
mongoose
  .connect(
    db,
    { useNewUrlParser: true }
  )
  .then(() => console.log("MongoDB connected"))
  .catch(err => console.log(err));

//Passport middleware
app.use(passport.initialize());

//Passport config
require("./config/passport")(passport);

app.use("/api/profile", profile);

const port = process.env.PORT || 5000;

app.listen(port, () => console.log(`server running on port ${port}`));

profile.js

const express = require("express");
const router = express.Router();
const passport = require("passport");
const multer = require("multer");

const parser = multer({ dest: "./images" });

router.post(
  "/player",
  [passport.authenticate("jwt", { session: false }), parser.single("file")],
  (req, res) => {

const newPlayer = {
  name: req.body.name,
  age: req.body.age,
  apodo: req.body.apodo,
  description: req.body.description,
  captain: req.body.captain,
  social: {
    instagram: req.body.instagram,
    facebook: req.body.facebook,
    twitter: req.body.twitter,
    youtube: req.body.youtube
  }
};
//set the Avatar for the player
console.log(req.file);
});

I would appreciate any help. Thank you.


Solution

  • You appear to have two problems:

    1. You aren't using the correct content type (axios by default will assume application/json and you need multipart/form-data)
    2. You seem to be assuming the upload will just work because it's part of a form, when you override onSubmit and call e.preventDefault() you cancel any default browser behaviour for that form and need to manually get the file to the server (I don't see any of that happening in your current code).

    Few changes need to happen to get this working, first step is to pass the file information to your action. The simplest way to do this is to add a ref to your file field

    <input ref={c => this.img = c} ...
    

    Then in the onSubmit function, you can use this ref to fetch the File object from the DOM to pass into the action payload

    onSubmit(e) {
      e.preventDefault();
    
      const playerData = {
        ...this.state,
        file: this.img.files[0]
      }
    
      this.props.addPlayer(playerData, this.props.history);
    }
    

    In the action, you need to send the data as a multipart/form-data request, to do this you can simply pass FormData to axios and let it handle setting the appropriate headers etc.

    export const addPlayer = (playerData, history) => dispatch => {
      // build form data
      const form = Object.keys(playerData).reduce((f, k) => {
        f.append(k, playerData[k]);
        return f;
      }, new FormData());
      // send request
      return axios
        .post("api/profile/player", form)
        .then(res => history.push("/dashboard"))
        .catch(err =>
          dispatch({
            type: GET_ERRORS,
            payload: err.response.data
          })
        );
    };
    

    Your server code should just work as is.