Subiendo archivos al servidor utilizando redux-form con Dropzone en ReactJS

Buenos días en estos días estaba buscando documentación sobre como hacer un formulario en ReactJS con redux-form pero que además me
permitiera la posibilidad de subir archivos hacia un servidor usando la librería Javascript Dropzone. A continuación les describo como quedó
finalmente el proyecto paso a paso.
1- Lo primero es crear el proyecto y agregar las dependencias:
- create-react-app form-redux-dropzone
- cd form-redux-dropzone
- npm install
- npm install react-toastify prop-types react-dropzone react-icons react-loading-overlay react-redux react-router-dom react-router-redux redux-form redux-logger redux-thunk
- npm start
2-La estructura del proyecto donde vamos a estar trabajando es la siguiente:
public
src
|__core
|_actions
|_reducers
|_service
|_helper
|__views
|_base
|_form-input
|_form
App.js
App.css
index.js
….
3-Ahora explicaremos que ficheros modificaremos o crearemos y con qué contenido:
1-Definir las actions de redux:
#src/core/actions/index.js
const types = {
HTTP_PENDING: 'HTTP_PENDING',
HTTP_ERROR: 'HTTP_ERROR',
HTTP_SUCCESS: 'HTTP_SUCCESS',
FILE_ADD: 'FILE_ADD',
FILE_REMOVE: 'FILE_REMOVE',
HTTP_PENDING_UPLOAD: 'HTTP_PENDING_UPLOAD',
HTTP_SUCCESS_UPLOAD: 'HTTP_SUCCESS_UPLOAD',
}
const actions = {
//HTTP
startHTTPRequest: () => ({
type: types.HTTP_PENDING,
}),
stopHTTPRequestError: (error) => ({
type: types.HTTP_ERROR,
error: error
}),
stopRequestSuccess: (payload) => ({
type: types.HTTP_SUCCESS,
payload: payload
}),
//File
fileAdd: (file) => ({
type: types.FILE_ADD,
file: file
}),
fileRemove: (listFiles) => ({
type: types.FILE_REMOVE,
listFiles: listFiles
}),
startUpload: () => ({
type: types.HTTP_PENDING_UPLOAD,
}),
stopUpload: (file) => ({
type: types.HTTP_SUCCESS_UPLOAD,
file: file
}),
}
export { types, actions };
2-Definir los reducers:
#src/core/reducers/fileReducers.js
import { types } from '../actions';
const initialState = {
listFiles: [],
};
export default (state = initialState, action) => {
switch (action.type) {
case types.FILE_ADD:
let listAdd = state.listFiles
listAdd.push(action.file)
return {
...state,
listFiles: listAdd
}
case types.FILE_REMOVE:
let listDelete = state.listFiles
let index = listDelete.findIndex(data => data.id === action.id);
listDelete.splice(index, 1)
return {
...state,
listFiles: listDelete
}
default:
return state;
}
}
#src/core/reducers/uploadReducers.js
import { types } from '../actions';
const initialState = {
pending: false,
file: {},
};
export default (state = initialState, action) => {
switch (action.type) {
case types.HTTP_PENDING_UPLOAD:
return {
...state,
pending: true
}
case types.HTTP_SUCCESS_UPLOAD:
return {
...state,
pending: false,
file: action.file
}
default:
return state;
}
}
#src/core/reducers/httpReducers.js
import { types } from '../actions';
const initialState = {
pending: false,
payload: {},
error: null
};
export default (state = initialState, action) => {
switch (action.type) {
case types.HTTP_PENDING:
return {
...state,
pending: true
}
case types.HTTP_SUCCESS:
return {
...state,
pending: false,
payload: action.payload }
case types.HTTP_ERROR:
return {
...state,
pending: false,
error: action.error,
}
default:
return state;
}
}
#src/core/reducers/index.js
import { combineReducers } from 'redux';
import httpReducer from './httpReducer'
import { reducer as formReducer } from "redux-form";
import { routerReducer } from 'react-router-redux';
import fileReducer from './fileReducer'
import uploadReducer from './uploadReducer'
export default combineReducers({
httpReducer,
fileReducer,
uploadReducer,
routing: routerReducer,
form: formReducer,
});
3-Definir el store:
#src/core/helper/store.js
import rootReducer from '../../core/reducers';
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk';
const loggerMiddleware = createLogger()
export const store = createStore(rootReducer,
applyMiddleware(
loggerMiddleware,
thunk
)
);
4-Definir el local storage:
#src/core/helper/storage.js
const save = (key, payload) => {
localStorage.setItem(key, JSON.stringify(payload))
}
const load = (key) => {
return JSON.parse(localStorage.getItem(key))
}
const remove = (key) => {
localStorage.removeItem(key)
}
export default { save, load, remove }
5-Definir el http client:
#src/core/services/service.js
import storage from "../helper/storage";
import { toast } from "react-toastify";
const post = (endpoint, body) => {
const requestOptions = {
method: 'POST',
headers: header(),
body: JSON.stringify(body)
};
return fetch(process.env.REACT_APP_BACKEND + endpoint, requestOptions).then(handleResponse)
}
const put = (endpoint, body) => {
const requestOptions = {
method: 'PUT',
headers: header(),
body: JSON.stringify(body)
};
return fetch(process.env.REACT_APP_BACKEND + endpoint, requestOptions)
.then(handleResponse)
}
const get = (endpoint) => {
const requestOptions = {
method: 'GET',
headers: header(),
};
return fetch(process.env.REACT_APP_BACKEND + endpoint, requestOptions)
.then(handleResponse)
}
const del = (endpoint) => {
const requestOptions = {
method: 'DELETE',
headers: header()
};
return fetch(process.env.REACT_APP_BACKEND + endpoint, requestOptions)
.then(handleResponse)
}
const header = () => {
let token;
let data = storage.load(storage.keys.auth)
if (data && data.access_token) {
token = data.access_token
}
let header = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
if (token) {
Object.assign(header, { 'Authorization': 'Bearer ' + token })
}
return header;
}
const handleResponse = (response) => {
if (!response.ok) {
mapErrorToMessage(response.clone())
throw response
} else {
return response.json().then(data => {
return data;
});
}
}
const mapErrorToMessage = (response) => {
let code = response.status
switch (code) {
case 404:
toast.error( 'Element not found')
break;
case 500:
toast.error( 'Internal Error Server (This message is only for development proyect)')
break;
default:
return response.json().then(data => {
if (!data.message) {
toast.error(response.statusText)
} else {
toast.error( data.message)
}
});
}
}
export default { post, put, del, get, mapErrorToMessage }
6- Definir los componentes de los campos del formulario en este caso usamos el framework de diseño Boostrap 4 y definimos un componente
para un campo de texto y otro para Dropzone.
#src/views/base/form-input/TextFiled.js
import React from 'react';
export const TextField = ({
label,
input,
type,
id,
disabled,
meta: { touched, invalid, error },
...custom
}) => (
<div>
<label htmlFor={id}>{label}</label>
<input {...input} id={id} disabled={disabled} className={(touched && invalid) ? 'form-control form-control-lg is-invalid' : 'form-control form-control-lg'} type={type} placeholder={label} />
{touched && ((error && <div className="invalid-feedback"> {error}</div>))}
</div>
)
#src/views/base/form-input/DropZoneField.js
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import DropZone from "react-dropzone";
import FilePreview from "./FilePreview";
import Placeholder from "./Placeholder";
import ShowError from "./ShowError";
import { compose } from 'redux';
import { connect } from 'react-redux';
import LoadingOverlay from 'react-loading-overlay';
const allowType = [
"image/jpeg",
"image/png",
"image/gif",
"image/bmp",
"application/zip",
"application/vnd.oasis.opendocument.presentation",
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.text",
"application/vnd.ms-excel",
"application/x-rar-compressed",
"text/plain"
]
const DropZoneField = (props) => {
const {
handleOnDrop, label, disabled, pendingUpload,
input: { onChange },
listFiles,
meta: { error, touched }
} = props
useEffect(() => {
onChange(listFiles)
}, [listFiles.length])
return (
<div className="preview-container">
<label >{label} </label>
<LoadingOverlay
active={pendingUpload}
spinner
text='Uploading file and updating file list...'
>
<DropZone
disabled={disabled || pendingUpload}
accept={allowType.join(",")}
className="upload-container"
onDrop={file => handleOnDrop(file, onChange)}
multiple={false}
>
{props => <div className="row">
<div style={{ padding: '10px' }} className="col-md-6 col-xs-12">
{listFiles && listFiles.length > 0 ? (<FilePreview />) : (<div className="empty-preview"> Not file uploaded</div>)
}
</div>
<div className="col-md-6 col-xs-12">
<Placeholder {...props} error={error} touched={touched} />
</div>
</div>
}
</DropZone>
<ShowError error={error} touched={touched} />
</LoadingOverlay>
</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
})
),
onChange: PropTypes.func,
touched: PropTypes.bool
};
let mapStateToProps = state => {
return {
...state,
listFiles: state.fileReducer.listFiles,
pendingUpload: state.uploadReducer.pending
}
}
const mapDispatchToProps = (dispatch) => ({});
let enhance = compose(
connect(mapStateToProps, mapDispatchToProps)
)
export default enhance(DropZoneField);
#src/views/base/form-input/FilePreview.js
#Crea una carpeta en el proyecto y pon dentro los iconos de los ficheros requeridos
#esta carpera esta en src/resource
import React from "react";
import PropTypes from "prop-types";
import { compose } from 'redux';
import { connect } from 'react-redux';
import { actions } from "../../../core/actions";
import pdf from '../../../resource/pdf.png'
import zip from '../../../resource/zip.jpeg'
import doc from '../../../resource/doc.jpeg'
const getIcon = (type, image) => {
switch (type) {
case "image/jpeg":
case "image/png":
case "image/gif":
case "image/bmp":
return image;
break;
case "application/zip":
case "application/x-rar-compressed":
return zip;
break;
case "application/pdf":
return pdf;
break;
case "application/vnd.oasis.opendocument.presentation":
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
case "application/msword":
case "application/vnd.oasis.opendocument.spreadsheet":
case "application/vnd.oasis.opendocument.text":
case "application/vnd.ms-excel":
case "text/plain":
return doc;
break;
}
}
const FilePreview = (props) => {
const { listFiles, delFile, pending } = props
let deleteFile = (id) => {
delFile(id)
}
return (listFiles.map(({ name, preview, humanSize, type, id }) => (
<div key={name} className="row items">
<div className="col-3">
<img width="40" src={getIcon(type, preview)} alt={name} />
</div>
<div className="col-5">
<span className="nameFile">{humanSize}</span>
<span className="nameFile">{name}</span>
</div>
<div className="col-4" style={{ textAlign: "right" }}>
<button disabled={pending} onClick={() => deleteFile(id)} className="btn btn-danger">X</button>
</div>
</div>
))
)
};
let mapStateToProps = state => {
return {
...state,
listFiles: state.fileReducer.listFiles,
pending: state.httpReducer.pending,
}
}
const mapDispatchToProps = (dispatch) => ({
delFile: (id) => {
dispatch(actions.fileRemove(id));
}
});
let enhance = compose(
connect(mapStateToProps, mapDispatchToProps)
)
export default enhance(FilePreview);
#src/views/base/form-input/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: "40px",marginTop:'50px' }} />
<p>Click or drag 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;
#src/views/base/form-input/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-file">{error} </div>
) : null;
ShowError.propTypes = {
error: PropTypes.string,
touched: PropTypes.bool
};
export default ShowError;
7- Crear el formulario
#src/views/form/Form.js
import React, { useRef, useEffect, useState } from 'react';
import { compose } from 'redux';
import { Field, reduxForm } from 'redux-form'
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'
import { TextArea } from '../../base/form-input/TextField';
import DropZoneField from '../../base/form-input/DropZoneField';
import { toast } from 'react-toastify';
import service from '../service';
import LoadingOverlay from 'react-loading-overlay';
export const required = value => (value || typeof value === 'number' ? undefined : 'Required')
export const atLeastOne = value =>
{
return (value && value.length===0 ? 'At least one file must be uploaded' : undefined)
}
const Form = props => {
const { handleSubmit, onSubmit, pending = true, upload, pendingUpload, match, } = props
const { token } = match.params
let handleOnDrop = (newFile) => {
if (newFile.length === 0) {
toast.info("Archivo no permitido")
} else {
upload(newFile[0])
}
}
return (
<section className="testimonial py-5" id="testimonial">
<div className="container">
<div className="row ">
<div className="col-md-12 py-5 formbox">
<LoadingOverlay
active={pending}
spinner
text='Sending data...'
>
<h4 className="pb-4">Please fill with your details</h4>
<form onSubmit={handleSubmit((values) => { onSubmit(values, token) })}>
<div className="form-row">
<div className="form-group col-md-6">
<Field
name="exampleField"
component={TextField}
label="Example Field"
validate={[required]}
disabled={pending}
type="text"
id="exampleField"
/>
</div>
<div className="form-group col-md-6">
<Field
name="exampleField2"
component={TextField}
label="Example Field #2"
validate={[required]}
disabled={pending}
type="text"
id="exampleField2"
/>
</div>
<div className="form-group col-md-12">
<Field
name="files"
component={DropZoneField}
label="Evidences"
disabled={pending}
validate={[atLeastOne]}
type="file"
id="files"
handleOnDrop={handleOnDrop}
/>
</div>
</div>
<div style={{ textAlign: "right" }} className="form-row">
<div className="form-group col-md-12">
<button disabled={pendingUpload} type="submit" className="btn btn-danger">Send</button>
</div>
</div>
</form>
</LoadingOverlay>
</div>
</div>
</div>
</section>
);
}
let mapStateToProps = state => {
return {
...state,
pending: state.httpReducer.pending,
pendingUpload: state.uploadReducer.pending
}
}
const mapDispatchToProps = (dispatch) => ({
onSubmit: (values) => {
//submit data form service
},
upload: file => {
//upload file service
},
});
let enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
reduxForm({
form: 'form',
}),
withRouter
)
export default enhance(Form);
7- Configurar el router App.js
#src/App.js
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css'
import Form from './views/form/Form';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
function App() {
return (
<div className="masthead">
<ToastContainer position={toast.POSITION.BOTTOM_RIGHT} />
<Router history={history}>
<Switch>
<Route path={`/`} component={Form} />
</Switch>
</Router>
</div>
);
}
export default App;
8- Configurar el index.js
#src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { store } from './core/helper/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Listo proyecto terminado!!!!