React + Redux - Jaki jest najlepszy sposób obsługi CRUD w komponencie formularza?

129

Mam jeden formularz, który służy do tworzenia, odczytywania, aktualizowania i usuwania. Stworzyłem 3 komponenty o tej samej formie, ale przekazuję im różne rekwizyty. Mam CreateForm.js, ViewForm.js (tylko do odczytu za pomocą przycisku usuwania) i UpdateForm.js.

Kiedyś pracowałem z PHP, więc zawsze robiłem to w jednej formie.

Do zarządzania sklepem używam React i Redux.

Kiedy jestem w komponencie CreateForm, przekazuję moim komponentom podrzędnym this props, createForm={true}aby nie wypełniać danych wejściowych wartością i nie wyłączać ich. W moim komponencie ViewForm przekazuję te rekwizyty readonly="readonly".

I mam inny problem z obszarem tekstu, który jest wypełniony wartością i nie można go zaktualizować. React textarea z wartością jest tylko do odczytu, ale wymaga aktualizacji

Jaka jest najlepsza struktura, aby mieć tylko jeden komponent, który obsługuje te różne stany formularza?

Czy masz jakieś rady, samouczki, filmy, prezentacje, którymi chcesz się podzielić?

Mike Boutin
źródło

Odpowiedzi:

115

Znalazłem pakiet Redux Form . Robi naprawdę dobrą robotę!

Możesz więc używać Redux z React-Redux .

Najpierw musisz utworzyć komponent formularza (oczywiście):

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;

Następnie łączysz komponent obsługujący formularz:

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

I dodaj reduktor formy redux do swoich połączonych reduktorów:

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;

A moduł walidatora wygląda tak:

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

Po wypełnieniu formularza, gdy chcesz wypełnić wszystkie pola jakimiś wartościami, możesz skorzystać z initializefunkcji:

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

Innym sposobem wypełniania formularzy jest ustawienie wartości początkowych.

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

Jeśli masz inny sposób, aby sobie z tym poradzić, po prostu zostaw wiadomość! Dziękuję Ci.

Mike Boutin
źródło
3
Zastanawiam się - nadal używasz redux-forms? Zastanawiam się, jak ta płytka skaluje się w porównaniu z formami reakcji
Ashley Coolman
2
Tak, nadal go używam! Naprawdę fajnie, stworzyłem bardzo duże formy i zadziałało # 1. Musisz tylko bardzo uważać na to, co przekazujesz jako rekwizyty do swoich komponentów i ich aktualizacji. Przepraszamy za opóźnienie odpowiedzi.
Mike Boutin
1
@MikeBoutin, czy mógłbyś rozwinąć tę ostrożność dotyczącą rekwizytów? Dzięki
Adam K Dean
Warto zauważyć, że nawet od wersji 6.4.3, jeśli używasz pełnego potencjału, wydajność redux-formjest fatalna we wszystkich wersjach IE, w tym Edge. Jeśli musisz to wesprzeć, poszukaj gdzie indziej.
Stephen Collins
2
Po prostu trzeba być bardzo rygorystycznym z shouldComponentUpdate, aby nie tworzyć opóźnień w formularzach
Mike Boutin
12

AKTUALIZACJA: to rok 2018 i zawsze będę używał tylko Formika (lub bibliotek podobnych do Formika)

Istnieje również formularz reaktywuj-redux ( krok po kroku ), który wydaje się zamieniać część javascript (& boilerplate) formularza redux- a z deklaracją znaczników. Wygląda dobrze, ale jeszcze go nie używałem.

Wytnij i wklej z pliku 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);

Edycja: porównanie

Dokumentacja formularza reaktora redux zapewnia porównanie z formą reduxu:

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

Ashley Coolman
źródło
4

Tym, którym nie zależy na ogromnej bibliotece do obsługi spraw związanych z formularzami, poleciłbym redux-form-utils .

Może generować wartości i zmieniać programy obsługi dla kontrolek formularzy, generować redukcje formularza, przydatne kreatory akcji do czyszczenia niektórych (lub wszystkich) pól itp.

Wszystko, co musisz zrobić, to złożyć je w swoim kodzie.

Używając redux-form-utils, skończysz z manipulacją formułą, taką jak:

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

Jednak to tylko biblioteka rozwiązuje problemu Ci Una Ri D, być może bardziej zintegrowany Tablekomponent jest antipate.

jasonslyvia
źródło
1

Kolejna rzecz dla tych, którzy chcą stworzyć w pełni kontrolowany komponent formy bez korzystania z ponadgabarytowej biblioteki.

ReduxFormHelper - mała klasa ES6, poniżej 100 linii:

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

Nie wykonuje całej pracy za Ciebie. Ułatwia jednak tworzenie, walidację i obsługę kontrolowanego komponentu formularza. Możesz po prostu skopiować i wkleić powyższy kod do swojego projektu lub zamiast tego dołączyć odpowiednią bibliotekę - redux-form-helper(wtyczka!).

Jak używać

Pierwszym krokiem jest dodanie określonych danych do stanu Redux, które będą reprezentować stan naszego formularza. Dane te będą zawierać bieżące wartości pól, a także zestaw flag błędów dla każdego pola w formularzu.

Stan formy można dodać do istniejącej redukcji lub zdefiniować w oddzielnej redukcji.

Ponadto konieczne jest zdefiniowanie konkretnej akcji inicjującej aktualizację stanu formularza oraz odpowiedniego kreatora akcji.

Przykład działania :

export const FORM_UPDATE = 'FORM_UPDATE' 

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

Przykład reduktora :

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

Drugim i ostatnim krokiem jest utworzenie komponentu kontenera dla naszego formularza i połączenie go z odpowiednią częścią stanu i akcji Redux.

Musimy również zdefiniować model formularza określający walidację pól formularza. Teraz tworzymy instancję ReduxFormHelperobiektu jako element składowy komponentu i przekazujemy tam nasz model formularza oraz wywołanie zwrotne wysyłające aktualizację stanu formularza.

Następnie w render()metodzie komponentu musimy powiązać zdarzenia każdego pola onChangei formularza odpowiednio onSubmitz metodami processField()i processForm(), a także wyświetlić bloki błędów dla każdego pola w zależności od flag błędów formularza w stanie.

Poniższy przykład wykorzystuje CSS z frameworka Twitter Bootstrap.

Przykład komponentu kontenera :

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)

Próbny

najdalszy
źródło