Думаю, большинство знает схему работы библиотеки redux: view > action > middlewares > reducers > state > view

Подробности здесь.

Хочу представить вашему вниманию библиотеку, которая работает по тому же принципу для форм.

image

Документация на английском. Устанавливаем:

 npm install redux-form

Подключаем в наше приложение:

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

const reducers = {
  // ваши редюсеры
  form: formReducer     // В state все данные формы будут храниться в свойстве form
}
const reducer = combineReducers(reducers)
const store = createStore(reducer)

Создаем форму:

import React, { Component } from 'react';
// берем компонент поля (Field) и провайдер для формы (reduxForm)
import { Field, reduxForm } from 'redux-form';

class Form extends Component {
    render(){
        // по умолчанию handleSubmit принимает функцию обработчик
        // reset скидывает значения до значений, заданных во время инициализации
        // в данном случае до undefined, так как значение не задано
        const {handleSubmit, reset} = this.props;

        const submit = (values) => console.log(values);

        return (
            <form onSubmit={handleSubmit(submit)}>
                {/* принимает имя поля, тип и остальные свойства, которые расмотрим позже*/}
                <Field name="title" component="input" type="text"/>
                <Field name="text" component="input" type="text"/>
                <div>
                    <button type="button" onClick={reset}>Очистить форму</button>
                    <button type="submit">Отправить форму</button>
                </div>
            </form>
        );
    }
}
Form = reduxForm({
    form: 'post', // имя формы в state (state.form.post)
})(Form);

export default Form;

Расмотрим ситуацию, когда нужно прокинуть обработчик с компонента уровнем выше:

Создадим компонент:

import React, { Component }  from 'react';

import Form from './Form'

class EditPost extends Component{
    constructor(props) {
        super(props);
    }
   
    handleSubmit = (values) => {
        console.log(values);
    };
    render() {
        let {post, dispatch} = this.props;
        return (
            <div>
                {/* передаем обработчик*/}
                <Form onSubmit={this.handleSubmit} />
            </div>
        );
    }
}

И изменим нашу форму:

// меняем <form onSubmit={handleSubmit(submit)}> на 
<form onSubmit={handleSubmit}>

Если нам надо задать значение, при инициализации используем actionCreator initialize, который принимает первым параметром название формы, вторым объект с значениями. Например, для статьи по id:

import React, { Component }  from 'react';
// подключаем метод
import {initialize} from 'redux-form';
import {connect} from 'react-redux';

import Form from './Form'

class EditPost extends Component{
    constructor(props) {
        super(props);
        // post = {title: " Текст заголовка ", text: " Текст статьи "}
        let {post, initializePost} = this.props;
        // инициализация
        initializePost(post);
    }
   
    handleSubmit = (values) => {
        console.log(values);
    };
    render() {
        return (
            <div>
                <Form onSubmit={this.handleSubmit} />
            </div>
        );
    }
}
// прокидываем в props функцию для инициализации формы
function mapDispatchToProps(dispatch){
    return {
        initializePost: function (post){
            dispatch(initialize('post', post));
        }
    }
}
// прокидываем в props объект для инициализаци формы
function mapStateToProps(state, ownProps){
    const id = ownProps.params.id;
    return {
       post: state.posts[id]
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(EditPost);

Остальные action creators можно посмотреть здесь.

Если нас не устраивает стандартное поле, мы можем передавать свой вариант верстки и действий:

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';

class Form extends Component {
   // функция, которая возвращает свою реализацию
   renderField = ({ input, label, type}) => (
        <div>
            <label>{label}</label>
            <div>
                <input
                    {...input} placeholder={label} type={type}/>
            </div>
        </div>
    );
    render(){
        const {handleSubmit, reset} = this.props;

        return (
            <form onSubmit={handleSubmit}>
                {/* принимает функцию с реализацией поля*/}
                <Field name="title" component={this.renderField}  label="Заголовок" type="text"/>
                <Field name="text" component={this.renderField}  label="Текст" type="text"/>
                <div>
                    <button type="button" onClick={reset}>Очистить форму</button>
                    <button type="submit">Отправить форму</button>
                </div>
            </form>
        );
    }
}
Form = reduxForm({
    form: 'post'
})(Form);

export default Form;

Подробнее про компонент Field.

Redux-form поддерживает три вида валидации:

  • Синхронная валидация
  • Асинхронная валидация
  • Валидация во время сабмита

Для синхронной и асинхронной валидации создадим файл formValidate.js:

// синхронная валидация
export const validate = values => {
    const errors = {};
    if(!values.text){
        errors.text = 'Поле обязательно для заполнения!';
    } else if (values.text.length < 15) {
        errors.text = 'Текст должен быть не менее 15 символов!'
    }
    // для синхронной валидации нужно вернуть объект с ошибками
    return errors
};

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
//асинхронная валидация
//принимает два параметра значения и redux dispatch
export const asyncValidate = (values/*, dispatch */) => {
    return sleep(1000) // имитация серверного ответа
        .then(() => {
            if (!values.title) {
                // для асинхронной валидации нужно бросить объект с ошибкой
                throw {title: 'Поле обязательно для заполнения!'}
            } else if (values.title.length > 10) {
                throw {title: 'Заголовок должен быть не более 10 символов!'}
            }
        })
};

Для валидации во время сабмита нужно изменить обработчик сабмита так, чтобы он возвращал промис:

import React, { Component }  from 'react';
// подключаем класс ошибки для формы
import {initialize, SubmissionError} from 'redux-form';
import {connect} from 'react-redux';
import Form from './Form';

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
class EditPost extends Component{
    constructor(props) {
        super(props);
    }
   
    handleSubmit = (values) => {
        /* возвращаем промис
           erros в нашем случае это объект, в котором ключ - это название поля с ошибкой
           Например, {title: "Неверно введен заголовок"}*/
        return sleep(1000) {// симуляция ответа сервера}
          .then(({errors, ...data}) => {
             if (errors) {
                // бросаем экземпляр класса ошибки с текстами ошибок
                // _error общая ошибка для формы
                throw new SubmissionError({ ...errors, _error: 'Статья не добавлена!' })
             } else {
                // ошибок нет, обрабатываем данные data
            }
       })
    };
    render() {
        return (
            <div>
                {/* передаем обработчик*/}
                <Form onSubmit={this.handleSubmit} />
            </div>
        );
    }
}
function mapDispatchToProps(dispatch){
    return {
        initializePost: function (post){
            dispatch(initialize('post', post));
        }
    }
}
function mapStateToProps(state, ownProps){
    const id = ownProps.params.id;
    return {
       post: state.posts[id]
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(EditPost);

А теперь подключим валидацию и организуем вывод ошибок:

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import {validate, asyncValidate} from '../formValidate';

class Form extends Component {
   renderField = ({ input, label, type, meta: { touched, error, warning }}) => (
        <div>
            <label>{label}</label>
            <div>
                <input {...input} placeholder={label} type={type}/>
                {/* ошибка для поля*/}
                {touched && ((error && <div>{error}</div>))}
            </div>
        </div>
    );
    render(){
        const {handleSubmit, reset, error} = this.props;

        return (
            <form onSubmit={handleSubmit}>
                {/* принимает функцию с реализацией поля*/}
                <Field name="title" component={this.renderField}  label="Заголовок" type="text"/>
                <Field name="text" component={this.renderField}  label="Текст" type="text"/>
                <div>
                    <button type="button" onClick={reset}>Очистить форму</button>
                    <button type="submit">Отправить форму</button>
                    {/*общая ошибка для формы*/}
                    {error && <div>{error}</div>}
                </div>
            </form>
        );
    }
}

Form = reduxForm({
    form: 'post',
    // подключение валидации
    validate,
    asyncValidate
})(Form);

export default Form;

Для тех, кто хочет посмотреть пример работы, делаем так:

 git clone https://github.com/BoryaMogila/koa_react_redux.git;
 git checkout redux-form;
 npm install;
 npm run-script run-with-build;

И пробуем CRUD приложение с использованием redux-form по ссылке localhost(127.0.0.1):4000/app/.

При асинхронной валидации возможен конфуз: при нажатии сабмита до ответа с сервера сабмит сработает.

В документации есть еще много интересного и полезного. Рекомендую к просмотру.

P.S.: Как всегда жду конструктива.
Поделиться с друзьями
-->

Комментарии (20)


  1. Akuma
    01.11.2016 12:10
    +6

    Может быть я что-то делаю не так, но я вообще не использую Redux для форм.

    Формы — в 90% случаях обычные компоненты, которые не знают про окружение, т.к. оно им и не нужно. А значит, хватает обычного this.state.
    Для зависимых форм данные либо подтягиваются снаружи, либо уж через connect() и props. Но Формы никогда не вызывают изменения Redux-store напрямую.


    1. BoryaMogila
      01.11.2016 12:20

      Это удобно и расширяемо. Все формы в одном месте, всегда есть доступ к ним извне, структурирование данных.
      Удобно использовать на формах каталогов.


    1. VasilioRuzanni
      01.11.2016 14:35

      Ну тут считайте, что формы просто используют Redux как хранилище состояния, вместо хранения его в компонентах. Там отдельный редьюсер, и эта часть state ничего не знает про остальные. С этой частью стейта обычно не работают напрямую (однако иногда это бывает удобно, например, вручную залить серверной валидации ошибки в стейт в обход того, что предлагает redux-form).


  1. DenimTornado
    01.11.2016 15:55
    +5

    «Прости им, ибо не ведают, что творят»


    1. DenimTornado
      01.11.2016 18:27
      +1

      Хм, не, ну а этом вот называется просто? Серьёзно?


      1. BoryaMogila
        01.11.2016 18:34

        Давай свое решение проще, которое позволит хорошо расширять логику, структурировать и доступатся к форме с других компонентов.


        1. DenimTornado
          01.11.2016 18:41
          +2

          Да на, берём так, открываем блокнот, пишем там:

          <form action="/" method="get">
            <input type="text" name="name">
            <input type="submit" value="Submit">
          </form>
          

          Моя претензия к модному заголовку. Просто работать с формами без какой-либо нагрузки. Без редуксов. А в твоём случае именно удобнее, потому что расширяемость и т.п.

          Задолбали уже посты про «просто с React», «легко с Redux», «воздушно с Angular». Расширяемо — да, удобно — наверно, но никак не просто! Калашников — простой, ЗРК Бук — расширяемый.


          1. BoryaMogila
            01.11.2016 18:55
            -4

            Во-первых редакс. Статья так называется потому, что это проще решение чем те, с которыми я встречался в flux архитектурах. А чего ты хотел, пишы html сайты, так проще всего. Во-вторых я вижу ты в оружии лучше разбираешься…


            1. DenimTornado
              01.11.2016 18:58
              +6

              Ясно, что в лоб, что по лбу, удачи с flux архитектурами.


  1. justboris
    01.11.2016 16:01

    Как в тему вчера вечером была опубликована похожая статья


    mgr-forms-react: Простой компонент для простейших форм


    Можно смотреть и сравнивать данные решения.


    Я не автор ни одной из этих статей, мимо проходил


  1. NeXTs_od
    01.11.2016 17:51

    redux-form хорош.
    можно настроить клиентскую валидацию, асинхронную серверную валидацию при изменении поля, при сабмите.
    унифицированный формат вывода ошибок, валидация через промисы, да много чего еще…
    в общем понравилось мне с ним работать


  1. 3axap4eHko
    01.11.2016 20:25
    +2

    redux-form это чистой воды антипаттерн, из самостоятельных компонентов создается месиво из зависимостей, которое нигде нельзя использовать, кроме как данного случая, что сводит reusability к нулю, а именно reusability главная фитча реакта. такое ощущение, что ребята из авангарда ангуляра тащут в реакт свои костыли и подпорки


    1. BoryaMogila
      01.11.2016 20:42
      -1

      По вашей логике вся связка react + redux месиво из зависимосей.
      А я считаю что переиспользовать в самый раз:

      handleSubmit = (values) => {
              /* возвращаем промис
                 erros в нашем случае это объект, в котором ключ - это название поля с ошибкой
                 Например, {title: "Неверно введен заголовок"}*/
              return sleep(1000) {// симуляция ответа сервера}
                .then(({errors, ...data}) => {
                   if (errors) {
                      // бросаем экземпляр класса ошибки с текстами ошибок
                      // _error общая ошибка для формы
                      throw new SubmissionError({ ...errors, _error: 'Статья не добавлена!' })
                   } else {
                      // ошибок нет, обрабатываем данные data
                  }
             })
          };
          render() {
              return (
                  <div>
                      {/* передаем обработчик*/}
                      <Form onSubmit={this.handleSubmit} />
                  </div>
              );
          }
      

      Написал обработчик сабмита и подключил компонент. Чем вам не переиспользование?


      1. 3axap4eHko
        01.11.2016 22:22
        -2

        Полнейший бред. Вы абсолютно не понимаете концепцию связки react-redux. И да у Вас это тоже месиво кода так как Вы не разделяете понятие component и container. Я на 100% уверен что в строке

        // ошибок нет, обрабатываем данные data
        

        Вы используете setState не ожидая что к этому моменту компонент может быть уже unmounted и если не вы то ваш коллега.
        Я не считаю нужным обсуждать это дальше.


        1. BoryaMogila
          01.11.2016 22:42
          -2

          Ваша увереность это ваше личное дело. Предложите свое решение для удобства роботы с формами.


          1. 3axap4eHko
            02.11.2016 06:27

            Это не уверенность, а опыт. И у моей команды нет проблем с формами при работе в связке React-Redux. И еще, API должно быть отдельным слоем, а не внутри формы и результатом API должен быть success или failed action


            1. BoryaMogila
              02.11.2016 08:49
              -1

              failed action как раз и создаётся при ловле SubmissionError, а success action реализуется

              // ошибок нет, обрабатываем данные data
              

              Работа с API в данной статье необсуждалась.
              При чем тут ваша команда?


              1. 3axap4eHko
                02.11.2016 16:45

                Не причем. Желаю Вам успехов в своем творчестве


                1. BoryaMogila
                  02.11.2016 17:11

                  спасибо, вам также.


  1. n0ne
    02.11.2016 10:41

    А мне тоже понравился, только я использую material-версию (-:
    Мне нравится: просто, удобно, красиво (-: