web-dev-qa-db-fra.com

React + Redux - Quelle est la meilleure façon de gérer CRUD dans un composant de formulaire?

J'ai un formulaire qui est utilisé pour créer, lire, mettre à jour et supprimer. J'ai créé 3 composants avec la même forme mais je leur passe des accessoires différents. J'ai eu CreateForm.js, ViewForm.js (en lecture seule avec le bouton Supprimer) et UpdateForm.js.

J'avais l'habitude de travailler avec PHP, donc je les faisais toujours sous une forme.

J'utilise React et Redux pour gérer le magasin.

Lorsque je suis dans le composant CreateForm, je passe à mes sous-composants, ce qui permet au createForm={true} de ne pas remplir les entrées avec une valeur et de ne pas les désactiver. Dans mon composant ViewForm, je passe cet accessoire readonly="readonly".

Et j'ai un autre problème avec un textarea qui est rempli avec une valeur et qui ne peut pas être mis à jour. Réagissez textarea avec value est en lecture seule mais doit être mis à jour

Quelle est la meilleure structure pour avoir un seul composant qui gère ces différents états de la forme?

Avez-vous des conseils, des tutoriels, des vidéos, des démos à partager?

126
Mike Boutin

J'ai trouvé le paquet Redux Form . Ça fait vraiment du bon travail!

Donc, vous pouvez utiliser Redux avec React-Redux .

Vous devez d’abord créer un composant de formulaire (évidemment):

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;

Ensuite, vous connectez le composant qui gère le formulaire:

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);

Et ajoutez le réducteur redux-form dans vos réducteurs combinés:

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;

Et le module de validation ressemble à ceci:

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;
}

Une fois le formulaire rempli, vous pouvez utiliser la fonction initialize pour remplir tous les champs avec certaines valeurs:

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

Une autre façon de remplir les formulaires consiste à définir les valeurs initiales.

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 vous avez un autre moyen de gérer cela, laissez un message! Je vous remercie.

114
Mike Boutin

UPDATE: sa 2018 et je n'utiliserai jamais que Formik (ou des bibliothèques de type Formik)

Il y a aussi react-redux-form ( pas à pas ), qui semble échanger quelques-uns de javascript de redux-form (= passe-partout) avec déclaration de balisage. Ça a l'air bien, mais je ne l'ai pas encore utilisé.

Un copier/coller du readme:

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);

Edit: Comparaison

Les documents react-redux-form fournissent une comparaison par rapport à redux-form:

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

10
Ashley Coolman

Pour ceux qui ne se soucient pas d'une énorme bibliothèque pour gérer les problèmes liés aux formulaires, je recommanderais redux-form-utils .

Il peut générer de la valeur et modifier les gestionnaires pour vos contrôles de formulaire, générer des réducteurs du formulaire, des créateurs d’action pratiques pour effacer certains (ou tous) les champs, etc.

Tout ce que vous avez à faire est de les assembler dans votre code.

En utilisant redux-form-utils, vous vous retrouvez avec une manipulation de formulaire comme suit:

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>
    );
  }
}

Cependant, cette bibliothèque ne résout que les problèmes C et U, pour R et D, peut-être qu'un composant Table plus intégré est à antipatier.

4
jasonslyvia

Juste une autre chose pour ceux qui veulent créer un composant de formulaire entièrement contrôlé sans utiliser une bibliothèque surdimensionnée.

ReduxFormHelper - une petite classe ES6, inférieure à 100 lignes:

_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}
  }
}
_

Cela ne fait pas tout le travail pour vous. Cependant, cela facilite la création, la validation et le traitement d'un composant de formulaire contrôlé. Vous pouvez simplement copier et coller le code ci-dessus dans votre projet ou à la place, inclure la bibliothèque respective - redux-form-helper (plug!).

Comment utiliser

La première étape consiste à ajouter des données spécifiques à l’état Redux qui représentera l’état de notre formulaire. Ces données incluront les valeurs de champ actuelles ainsi qu'un ensemble d'indicateurs d'erreur pour chaque champ du formulaire.

L'état de la forme peut être ajouté à un réducteur existant ou défini dans un réducteur séparé.

De plus, il est nécessaire de définir une action spécifique à l'origine de la mise à jour de l'état du formulaire, ainsi que son créateur.

Exemple d'action :

_export const FORM_UPDATE = 'FORM_UPDATE' 

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

Exemple de réduction :

_...
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 || {})
      }
    ...
  }
}
_

La deuxième et dernière étape consiste à créer un composant conteneur pour notre formulaire et à le connecter à la partie respective de l'état et des actions Redux.

Nous devons également définir un modèle de formulaire spécifiant la validation des champs de formulaire. Nous instancions maintenant l'objet ReduxFormHelper en tant que membre du composant et y passons notre modèle de formulaire et une mise à jour de répartition par callback de l'état du formulaire.

Ensuite, dans la méthode render() du composant, nous devons associer les événements onChange de chaque champ et les événements onSubmit du formulaire avec les méthodes processField() et processForm(), ainsi que les blocs d'erreur d'affichage. pour chaque champ en fonction des drapeaux d'erreur de forme dans l'état.

L'exemple ci-dessous utilise CSS à partir du framework Bootstrap de Twitter.

Exemple de composant de conteneur :

_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)
_

Démo

1
hindmost