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

128

J'ai un formulaire 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 CreateForm.js, ViewForm.js (en lecture seule avec le bouton Supprimer) et UpdateForm.js.

J'avais l'habitude de travailler avec PHP, donc je l'ai toujours fait sous une seule forme.

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

Quand je suis dans le composant CreateForm, je passe à mes sous-composants ces accessoires createForm={true}pour ne pas remplir les entrées avec une valeur et ne pas les désactiver. Dans mon composant ViewForm, je passe ces accessoires readonly="readonly".

Et j'ai eu un autre problème avec une zone de texte qui est remplie avec une valeur et qui ne peut pas être mise à jour. La zone de texte de réaction avec la valeur est en lecture seule mais doit être mise à jour

Quelle est la meilleure structure pour n'avoir qu'un seul composant qui gère ces différents états du formulaire?

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

Mike Boutin
la source

Réponses:

115

J'ai trouvé le package Redux Form . Cela fait un très bon travail!

Ainsi, 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;

Après cela, 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, lorsque vous souhaitez remplir tous les champs avec des valeurs, vous pouvez utiliser la initializefonction:

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 simplement un message! Je vous remercie.

Mike Boutin
la source
3
Vous vous demandez simplement - utilisez-vous toujours redux-forms? Je me demande comment ce passe-partout passe par rapport aux formes de réaction
Ashley Coolman
2
Oui, je l'utilise toujours! Vraiment sympa, j'ai créé de très gros formulaires et cela a fonctionné # 1. Vous devez juste faire très attention à ce que vous passez comme accessoires à vos composants et à leurs mises à jour. Désolé pour le retard de la réponse.
Mike Boutin
1
@MikeBoutin pourriez-vous élaborer sur cette mise en garde concernant les accessoires? Merci
Adam K Dean
Il convient de souligner que même à partir de la v6.4.3, si vous utilisez à plein potentiel, les performances de redux-formsont abyssales sur toutes les versions d'IE, y compris Edge. Si vous devez le soutenir, cherchez ailleurs.
Stephen Collins
2
C'est juste pour être très strict avec shouldComponentUpdate, pour ne pas créer de retards dans vos formulaires
Mike Boutin
11

MISE À JOUR: c'est 2018 et je n'utiliserai jamais que Formik (ou des bibliothèques de type Formik)

Il y a aussi react-redux-form ( étape par étape ), qui semble échanger une partie du javascript (et passe-partout) de redux-form avec une déclaration de balisage. Cela 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 avec redux-form:

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

Ashley Coolman
la source
4

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 de vos contrôles de formulaire, générer des réducteurs de formulaire, des créateurs d'actions pratiques pour effacer certains (ou tous) 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 le problème Cet U, pour Ret D, peut-être qu'un Tablecomposant plus intégré est d'antipate.

jasonslyvia
la source
1

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

ReduxFormHelper - une petite classe ES6, moins de 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, il facilite la création, la validation et la gestion 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ésenteront l'état de notre formulaire. Ces données comprendront les valeurs de champ actuelles ainsi qu'un ensemble d'indicateurs d'erreur pour chaque champ du formulaire.

L'état du formulaire 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 déclenchant la mise à jour de l'état du formulaire ainsi que le créateur d'action respectif.

Exemple d'action :

export const FORM_UPDATE = 'FORM_UPDATE' 

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

Exemple de réducteur :

...
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 avec une 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. Maintenant, nous instancions ReduxFormHelperobject en tant que membre du composant et y passons notre modèle de formulaire et une mise à jour de distribution de rappel de l'état du formulaire.

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

L'exemple ci-dessous utilise le CSS du framework Twitter Bootstrap.

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

le plus en arrière
la source