React + Redux: ¿cuál es la mejor manera de manejar CRUD en un componente de formulario?

128

Tengo un formulario que está acostumbrado a Crear, Leer, Actualizar y Eliminar. Creé 3 componentes con la misma forma pero les paso diferentes accesorios. Obtuve CreateForm.js, ViewForm.js (solo lectura con el botón Eliminar) y UpdateForm.js.

Solía ​​trabajar con PHP, así que siempre hacía esto de una forma.

Yo uso React y Redux para administrar la tienda.

Cuando estoy en el componente CreateForm, paso a mis subcomponentes estos accesorios createForm={true}para no llenar las entradas con un valor y no deshabilitarlas. En mi componente ViewForm, paso estos accesorios readonly="readonly".

Y tengo otro problema con un área de texto que está lleno de un valor y no es actualizable. Reaccionar el área de texto con valor es de solo lectura pero debe actualizarse

¿Cuál es la mejor estructura para tener un solo componente que maneje estos diferentes estados del formulario?

¿Tienes algún consejo, tutoriales, videos, demos para compartir?

Mike Boutin
fuente

Respuestas:

115

Encontré el paquete Redux Form . ¡Hace un muy buen trabajo!

Entonces, puedes usar Redux con React-Redux .

Primero debes crear un componente de formulario (obviamente):

import React from 'react';
import { reduxForm } from 'redux-form';
import validateContact from '../utils/validateContact';

class ContactForm extends React.Component {
  render() {
    const { fields: {name, address, phone}, handleSubmit } = this.props;
    return (
      <form onSubmit={handleSubmit}>
        <label>Name</label>
        <input type="text" {...name}/>
        {name.error && name.touched && <div>{name.error}</div>}

        <label>Address</label>
        <input type="text" {...address} />
        {address.error && address.touched && <div>{address.error}</div>}

        <label>Phone</label>
        <input type="text" {...phone}/>
        {phone.error && phone.touched && <div>{phone.error}</div>}

        <button onClick={handleSubmit}>Submit</button>
      </form>
    );
  }
}

ContactForm = reduxForm({
  form: 'contact',                      // the name of your form and the key to
                                        // where your form's state will be mounted
  fields: ['name', 'address', 'phone'], // a list of all your fields in your form
  validate: validateContact             // a synchronous validation function
})(ContactForm);

export default ContactForm;

Después de esto, conecta el componente que maneja el formulario:

import React from 'react';
import { connect } from 'react-redux';
import { initialize } from 'redux-form';
import ContactForm from './ContactForm.react';

class App extends React.Component {

  handleSubmit(data) {
    console.log('Submission received!', data);
    this.props.dispatch(initialize('contact', {})); // clear form
  }

  render() {
    return (
      <div id="app">
        <h1>App</h1>
        <ContactForm onSubmit={this.handleSubmit.bind(this)}/>
      </div>
    );
  }

}

export default connect()(App);

Y agregue el reductor de forma redux en sus reductores combinados:

import { combineReducers } from 'redux';
import { appReducer } from './app-reducers';
import { reducer as formReducer } from 'redux-form';

let reducers = combineReducers({
  appReducer, form: formReducer // this is the form reducer
});

export default reducers;

Y el módulo validador se ve así:

export default function validateContact(data, props) {
  const errors = {};
  if(!data.name) {
    errors.name = 'Required';
  }
  if(data.address && data.address.length > 50) {
    errors.address = 'Must be fewer than 50 characters';
  }
  if(!data.phone) {
    errors.phone = 'Required';
  } else if(!/\d{3}-\d{3}-\d{4}/.test(data.phone)) {
    errors.phone = 'Phone must match the form "999-999-9999"'
  }
  return errors;
}

Después de completar el formulario, cuando desee completar todos los campos con algunos valores, puede usar la initializefunción:

componentWillMount() {
  this.props.dispatch(initialize('contact', {
    name: 'test'
  }, ['name', 'address', 'phone']));
}

Otra forma de llenar los formularios es establecer los valores iniciales.

ContactForm = reduxForm({
  form: 'contact',                      // the name of your form and the key to
  fields: ['name', 'address', 'phone'], // a list of all your fields in your form
  validate: validateContact             // a synchronous validation function
}, state => ({
  initialValues: {
    name: state.user.name,
    address: state.user.address,
    phone: state.user.phone,
  },
}))(ContactForm);

Si tienes otra forma de manejar esto, ¡solo deja un mensaje! Gracias.

Mike Boutin
fuente
3
Solo me pregunto: ¿sigues usando redux-forms? Me pregunto cómo se escala esa
placa repetitiva en
2
Sí, todavía lo estoy usando! Realmente agradable, creé formas muy grandes y funcionó # 1. Solo debes tener mucho cuidado con lo que pasas como accesorios a tus componentes y sus actualizaciones. Perdón por el retraso de la respuesta.
Mike Boutin el
1
@MikeBoutin, ¿podría explicar esa precaución con respecto a los accesorios? Gracias
Adam K Dean
Vale la pena señalar que incluso a partir de v6.4.3, si está utilizando todo su potencial, el rendimiento de redux-formes abismal en todas las versiones de IE, incluido Edge. Si tiene que soportarlo, busque en otro lado.
Stephen Collins el
2
Es solo ser muy estricto con shouldComponentUpdate, no crear retrasos en sus formularios
Mike Boutin
11

ACTUALIZACIÓN: es 2018 y solo usaré Formik (o bibliotecas similares a Formik)

También hay react-redux-form ( paso a paso ), que parece intercambiar algunos de javascript (& boilerplate) de redux-form con declaración de marcado. Se ve bien, pero aún no lo he usado.

Un cortar y pegar del archivo Léame:

import React from 'react';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { modelReducer, formReducer } from 'react-redux-form';

import MyForm from './components/my-form-component';

const store = createStore(combineReducers({
  user: modelReducer('user', { name: '' }),
  userForm: formReducer('user')
}));

class App extends React.Component {
  render() {
    return (
      <Provider store={ store }>
        <MyForm />
      </Provider>
    );
  }
}

./components/my-form-component.js

import React from 'react';
import { connect } from 'react-redux';
import { Field, Form } from 'react-redux-form';

class MyForm extends React.Component {
  handleSubmit(val) {
    // Do anything you want with the form value
    console.log(val);
  }

  render() {
    let { user } = this.props;

    return (
      <Form model="user" onSubmit={(val) => this.handleSubmit(val)}>
        <h1>Hello, { user.name }!</h1>
        <Field model="user.name">
          <input type="text" />
        </Field>
        <button>Submit!</button>
      </Form>
    );
  }
}

export default connect(state => ({ user: state.user }))(MyForm);

Editar: Comparación

Los documentos react-redux-form proporcionan una comparación frente a la forma redux:

https://davidkpiano.github.io/react-redux-form/docs/guides/compare-redux-form.html

Ashley Coolman
fuente
4

Para aquellos a quienes no les importa una enorme biblioteca para manejar problemas relacionados con formularios, recomendaría redux-form-utils .

Puede generar valor y cambiar manejadores para sus controles de formulario, generar reductores del formulario, creadores de acciones útiles para borrar ciertos (o todos) campos, etc.

Todo lo que necesitas hacer es ensamblarlos en tu código.

Al usarlo redux-form-utils, terminas con una manipulación de formularios como la siguiente:

import { createForm } from 'redux-form-utils';

@createForm({
  form: 'my-form',
  fields: ['name', 'address', 'gender']
})
class Form extends React.Component {
  render() {
    const { name, address, gender } = this.props.fields;
    return (
      <form className="form">
        <input name="name" {...name} />
        <input name="address" {...address} />
        <select {...gender}>
          <option value="male" />
          <option value="female" />
        </select>
      </form>
    );
  }
}

Sin embargo, esta biblioteca solo resuelve el problema Cy U, para Ry D, quizás un Tablecomponente más integrado es antipatear.

jasonslyvia
fuente
1

Solo otra cosa para aquellos que desean crear un componente de formulario totalmente controlado sin usar una biblioteca de gran tamaño.

ReduxFormHelper : una pequeña clase ES6, menos de 100 líneas:

class ReduxFormHelper {
  constructor(props = {}) {
    let {formModel, onUpdateForm} = props
    this.props = typeof formModel === 'object' &&
      typeof onUpdateForm === 'function' && {formModel, onUpdateForm}
  }

  resetForm (defaults = {}) {
    if (!this.props) return false
    let {formModel, onUpdateForm} = this.props
    let data = {}, errors = {_flag: false}
    for (let name in formModel) {
      data[name] = name in defaults? defaults[name] :
        ('default' in formModel[name]? formModel[name].default : '')
      errors[name] = false
    }
    onUpdateForm(data, errors)
  }

  processField (event) {
    if (!this.props || !event.target) return false
    let {formModel, onUpdateForm} = this.props
    let {name, value, error, within} = this._processField(event.target, formModel)
    let data = {}, errors = {_flag: false}
    if (name) {
      value !== false && within && (data[name] = value)
      errors[name] = error
    }
    onUpdateForm(data, errors)
    return !error && data
  }

  processForm (event) {
    if (!this.props || !event.target) return false
    let form = event.target
    if (!form || !form.elements) return false
    let fields = form.elements
    let {formModel, onUpdateForm} = this.props
    let data = {}, errors = {}, ret = {}, flag = false
    for (let n = fields.length, i = 0; i < n; i++) {
      let {name, value, error, within} = this._processField(fields[i], formModel)
      if (name) {
        value !== false && within && (data[name] = value)
        value !== false && !error && (ret[name] = value)
        errors[name] = error
        error && (flag = true)
      }
    }
    errors._flag = flag
    onUpdateForm(data, errors)
    return !flag && ret
  }

  _processField (field, formModel) {
    if (!field || !field.name || !('value' in field))
      return {name: false, value: false, error: false, within: false}
    let name = field.name
    let value = field.value
    if (!formModel || !formModel[name])
      return {name, value, error: false, within: false}
    let model = formModel[name]
    if (model.required && value === '')
      return {name, value, error: 'missing', within: true}
    if (model.validate && value !== '') {
      let fn = model.validate
      if (typeof fn === 'function' && !fn(value))
        return {name, value, error: 'invalid', within: true}
    }
    if (model.numeric && isNaN(value = Number(value)))
      return {name, value: 0, error: 'invalid', within: true}
    return {name, value, error: false, within: true}
  }
}

No hace todo el trabajo por ti. Sin embargo, facilita la creación, validación y manejo de un componente de formulario controlado. Puede copiar y pegar el código anterior en su proyecto o, en su lugar, incluir la biblioteca respectiva - redux-form-helper(¡enchufe!).

Cómo utilizar

El primer paso es agregar datos específicos al estado de Redux que representarán el estado de nuestro formulario. Estos datos incluirán valores de campo actuales, así como un conjunto de indicadores de error para cada campo en el formulario.

El estado del formulario puede agregarse a un reductor existente o definirse en un reductor separado.

Además, es necesario definir una acción específica que inicie la actualización del estado del formulario, así como el creador de la acción correspondiente.

Ejemplo de acción :

export const FORM_UPDATE = 'FORM_UPDATE' 

export const doFormUpdate = (data, errors) => {
  return { type: FORM_UPDATE, data, errors }
}
...

Ejemplo reductor :

...
const initialState = {
  formData: {
    field1: '',
    ...
  },
  formErrors: {
  },
  ...
}

export default function reducer (state = initialState, action) {
  switch (action.type) {
    case FORM_UPDATE:
      return {
        ...ret,
        formData: Object.assign({}, formData, action.data || {}),
        formErrors: Object.assign({}, formErrors, action.errors || {})
      }
    ...
  }
}

El segundo y último paso es crear un componente contenedor para nuestro formulario y conectarlo con la parte respectiva del estado y las acciones de Redux.

También necesitamos definir un modelo de formulario que especifique la validación de los campos de formulario. Ahora instanciamos el ReduxFormHelperobjeto como miembro del componente y pasamos allí nuestro modelo de formulario y una actualización de envío de devolución de llamada del estado del formulario.

Luego, en el render()método del componente, debemos vincular onChangelos onSubmiteventos de cada campo y del formulario con los métodos processField()y processForm()respectivamente, así como mostrar los bloques de error para cada campo dependiendo de los indicadores de error del formulario en el estado.

El siguiente ejemplo utiliza CSS del marco de Twitter Bootstrap.

Ejemplo de componente de contenedor :

import React, {Component} from 'react';
import {connect} from 'react-redux'
import ReduxFormHelper from 'redux-form-helper'

class MyForm extends Component {
  constructor(props) {
    super(props);
    this.helper = new ReduxFormHelper(props)
    this.helper.resetForm();
  }

  onChange(e) {
    this.helper.processField(e)
  }

  onSubmit(e) {
    e.preventDefault()
    let {onSubmitForm} = this.props
    let ret = this.helper.processForm(e)
    ret && onSubmitForm(ret)
  }

  render() {
    let {formData, formErrors} = this.props
    return (
  <div>
    {!!formErrors._flag &&
      <div className="alert" role="alert">
        Form has one or more errors.
      </div>
    }
    <form onSubmit={this.onSubmit.bind(this)} >
      <div className={'form-group' + (formErrors['field1']? ' has-error': '')}>
        <label>Field 1 *</label>
        <input type="text" name="field1" value={formData.field1} onChange={this.onChange.bind(this)} className="form-control" />
        {!!formErrors['field1'] &&
        <span className="help-block">
          {formErrors['field1'] === 'invalid'? 'Must be a string of 2-50 characters' : 'Required field'}
        </span>
        }
      </div>
      ...
      <button type="submit" className="btn btn-default">Submit</button>
    </form>
  </div>
    )
  }
}

const formModel = {
  field1: {
    required: true,
    validate: (value) => value.length >= 2 && value.length <= 50
  },
  ...
}

function mapStateToProps (state) {
  return {
    formData: state.formData, formErrors: state.formErrors,
    formModel
  }
}

function mapDispatchToProps (dispatch) {
  return {
    onUpdateForm: (data, errors) => {
      dispatch(doFormUpdate(data, errors))
    },
    onSubmitForm: (data) => {
      // dispatch some action which somehow updates state with form data
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyForm)

Manifestación

postrero
fuente