Redux-form. Когда работать с формами просто

Думаю, большинство знает схему работы библиотеки 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 = 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 (
            
{/* передаем обработчик*/}
); } }

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


Если нам надо задать значение, при инициализации используем 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 (
            
); } } // прокидываем в 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}) => (
        
); render(){ const {handleSubmit, reset} = this.props; return ( {/* принимает функцию с реализацией поля*/}
); } } 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 (
            
{/* передаем обработчик*/}
); } } 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 }}) => (
        
{/* ошибка для поля*/} {touched && ((error &&
{error}
))}
); render(){ const {handleSubmit, reset, error} = this.props; return ( {/* принимает функцию с реализацией поля*/}
{/*общая ошибка для формы*/} {error &&
{error}
}
); } } 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.: Как всегда жду конструктива.

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

  • 1 ноября 2016 в 12:10 (комментарий был изменён)

    +2

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

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

    • 1 ноября 2016 в 12:20

      0

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

© Habrahabr.ru