React + Redux - Wie gehe ich am besten mit CRUD in einer Formularkomponente um?

128

Ich habe ein Formular zum Erstellen, Lesen, Aktualisieren und Löschen. Ich habe 3 Komponenten mit der gleichen Form erstellt, aber ich gebe ihnen verschiedene Requisiten. Ich habe CreateForm.js, ViewForm.js (schreibgeschützt mit der Schaltfläche Löschen) und UpdateForm.js.

Ich habe früher mit PHP gearbeitet, also habe ich diese immer in einer Form gemacht.

Ich verwende React und Redux, um den Shop zu verwalten.

Wenn ich mich in der CreateForm-Komponente befinde, übergebe ich diese Requisiten an meine Unterkomponenten createForm={true}, um die Eingaben nicht mit einem Wert zu füllen und sie nicht zu deaktivieren. In meiner ViewForm-Komponente übergebe ich diese Requisiten readonly="readonly".

Und ich habe ein weiteres Problem mit einem Textbereich, der mit einem Wert gefüllt und nicht aktualisierbar ist. Der Textbereich mit Wert reagieren ist schreibgeschützt, muss jedoch aktualisiert werden

Was ist die beste Struktur, um nur eine Komponente zu haben, die diese verschiedenen Zustände des Formulars verarbeitet?

Haben Sie Ratschläge, Tutorials, Videos und Demos zum Teilen?

Mike Boutin
quelle

Antworten:

115

Ich habe das Redux Form- Paket gefunden. Es macht einen wirklich guten Job!

Sie können also Redux mit React-Redux verwenden .

Zuerst müssen Sie (offensichtlich) eine Formularkomponente erstellen:

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;

Danach verbinden Sie die Komponente, die das Formular verarbeitet:

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

Und fügen Sie den Redux-Form-Reduzierer zu Ihren kombinierten Reduzierern hinzu:

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;

Und das Validator-Modul sieht folgendermaßen aus:

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

Wenn Sie nach dem Ausfüllen des Formulars alle Felder mit einigen Werten füllen möchten, können Sie die folgende initializeFunktion verwenden:

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

Eine andere Möglichkeit, die Formulare auszufüllen, besteht darin, die Anfangswerte festzulegen.

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

Wenn Sie eine andere Möglichkeit haben, dies zu tun, hinterlassen Sie einfach eine Nachricht! Danke dir.

Mike Boutin
quelle
3
Ich frage mich nur - verwenden Sie noch redux-forms? Ich frage mich, wie diese Kesselplatte im Vergleich zu Reaktionsformen skaliert
Ashley Coolman
2
Ja, ich benutze es immer noch! Wirklich schön, ich habe sehr große Formulare erstellt und es hat # 1 funktioniert. Sie müssen nur sehr vorsichtig sein mit dem, was Sie als Requisiten an Ihre Komponenten und deren Updates übergeben. Entschuldigen Sie die Verzögerung der Antwort.
Mike Boutin
1
@ MikeBoutin Könnten Sie diese Vorsicht in Bezug auf Requisiten näher erläutern? Vielen Dank
Adam K Dean
Es ist erwähnenswert, dass sogar von v6.4.3, wenn Sie es verwenden volles Potenzial ist, die Leistung redux-formist miserabel auf allen Versionen von IE, einschließlich Rand. Wenn Sie es unterstützen müssen, schauen Sie woanders hin.
Stephen Collins
2
Es ist nur sehr streng mit shouldComponentUpdate, keine Verzögerungen in Ihren Formularen zu erstellen
Mike Boutin
11

UPDATE: Es ist 2018 und ich werde immer nur Formik (oder Formik-ähnliche Bibliotheken) verwenden.

Es gibt auch eine React-Redux-Form ( Schritt für Schritt ), die einen Teil der Redux-Form auszutauschen scheint Javascript (& Boilerplate) gegen eine Markup-Deklaration . Es sieht gut aus, aber ich habe es noch nicht benutzt.

Ein Ausschneiden und Einfügen aus der Readme-Datei:

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

Bearbeiten: Vergleich

Die React-Redux-Form-Dokumente bieten einen Vergleich mit der Redux-Form:

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

Ashley Coolman
quelle
4

Für diejenigen, die sich nicht für eine riesige Bibliothek zur Behandlung formularbezogener Probleme interessieren, würde ich redux-form-utils empfehlen .

Es kann Werte generieren und Handler für Ihre Formularsteuerelemente ändern, Reduzierungen des Formulars generieren, praktische Aktionsersteller zum Löschen bestimmter (oder aller) Felder usw.

Alles was Sie tun müssen, ist sie in Ihrem Code zusammenzusetzen.

Wenn Sie verwenden redux-form-utils, erhalten Sie eine Formularmanipulation wie folgt:

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

Diese Bibliothek löst jedoch nur das Problem, Cund Ufür Rund Dmöglicherweise ist eine stärker integrierte TableKomponente das Antipieren.

jasonslyvia
quelle
1

Nur eine andere Sache für diejenigen, die eine vollständig kontrollierte Formularkomponente erstellen möchten, ohne eine übergroße Bibliothek zu verwenden.

ReduxFormHelper - eine kleine ES6-Klasse mit weniger als 100 Zeilen:

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

Es erledigt nicht die ganze Arbeit für Sie. Es erleichtert jedoch die Erstellung, Validierung und Handhabung einer kontrollierten Formularkomponente. Sie können den obigen Code einfach kopieren und in Ihr Projekt einfügen oder stattdessen die entsprechende Bibliothek hinzufügen -redux-form-helper (Plug!).

Wie benutzt man

Der erste Schritt besteht darin, dem Redux-Status bestimmte Daten hinzuzufügen, die den Status unseres Formulars darstellen. Diese Daten enthalten aktuelle Feldwerte sowie eine Reihe von Fehlerflags für jedes Feld im Formular.

Der Formularstatus kann zu einem vorhandenen Reduzierer hinzugefügt oder in einem separaten Reduzierer definiert werden.

Darüber hinaus müssen Sie eine bestimmte Aktion definieren, die die Aktualisierung des Formularstatus initiiert, sowie den jeweiligen Aktionsersteller.

Aktionsbeispiel :

export const FORM_UPDATE = 'FORM_UPDATE' 

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

Reduzierbeispiel :

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

Der zweite und letzte Schritt besteht darin, eine Containerkomponente für unser Formular zu erstellen und diese mit dem jeweiligen Teil des Redux-Status und der Aktionen zu verbinden.

Außerdem müssen wir ein Formularmodell definieren, das die Validierung von Formularfeldern spezifiziert. Jetzt instanziieren wirReduxFormHelper Objekt als Mitglied der Komponente und übergeben dort unser Formularmodell und eine Rückruf-Dispatching-Aktualisierung des Formularstatus.

Dann render()müssen wir in der Methode der Komponente onChangedie onSubmitEreignisse jedes Felds und des Formulars mit bindenprocessField() bzw. processForm()Methoden verknüpfen und Fehlerblöcke für jedes Feld anzeigen, abhängig von den Formularfehlerflags im Status.

Im folgenden Beispiel wird CSS aus dem Twitter Bootstrap-Framework verwendet.

Beispiel für eine Containerkomponente :

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)

Demo

hinterste
quelle