Подробности здесь.
Хочу представить вашему вниманию библиотеку, которая работает по тому же принципу для форм.
Документация на английском. Устанавливаем:
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)
DenimTornado
01.11.2016 15:55+5«Прости им, ибо не ведают, что творят»
DenimTornado
01.11.2016 18:27+1Хм, не, ну а этом вот называется просто? Серьёзно?
BoryaMogila
01.11.2016 18:34Давай свое решение проще, которое позволит хорошо расширять логику, структурировать и доступатся к форме с других компонентов.
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». Расширяемо — да, удобно — наверно, но никак не просто! Калашников — простой, ЗРК Бук — расширяемый.BoryaMogila
01.11.2016 18:55-4Во-первых редакс. Статья так называется потому, что это проще решение чем те, с которыми я встречался в flux архитектурах. А чего ты хотел, пишы html сайты, так проще всего. Во-вторых я вижу ты в оружии лучше разбираешься…
justboris
01.11.2016 16:01Как в тему вчера вечером была опубликована похожая статья
mgr-forms-react: Простой компонент для простейших форм
Можно смотреть и сравнивать данные решения.
Я не автор ни одной из этих статей, мимо проходил
NeXTs_od
01.11.2016 17:51redux-form хорош.
можно настроить клиентскую валидацию, асинхронную серверную валидацию при изменении поля, при сабмите.
унифицированный формат вывода ошибок, валидация через промисы, да много чего еще…
в общем понравилось мне с ним работать
3axap4eHko
01.11.2016 20:25+2redux-form это чистой воды антипаттерн, из самостоятельных компонентов создается месиво из зависимостей, которое нигде нельзя использовать, кроме как данного случая, что сводит reusability к нулю, а именно reusability главная фитча реакта. такое ощущение, что ребята из авангарда ангуляра тащут в реакт свои костыли и подпорки
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> ); }
Написал обработчик сабмита и подключил компонент. Чем вам не переиспользование?3axap4eHko
01.11.2016 22:22-2Полнейший бред. Вы абсолютно не понимаете концепцию связки react-redux. И да у Вас это тоже месиво кода так как Вы не разделяете понятие component и container. Я на 100% уверен что в строке
// ошибок нет, обрабатываем данные data
Вы используете setState не ожидая что к этому моменту компонент может быть уже unmounted и если не вы то ваш коллега.
Я не считаю нужным обсуждать это дальше.BoryaMogila
01.11.2016 22:42-2Ваша увереность это ваше личное дело. Предложите свое решение для удобства роботы с формами.
3axap4eHko
02.11.2016 06:27Это не уверенность, а опыт. И у моей команды нет проблем с формами при работе в связке React-Redux. И еще, API должно быть отдельным слоем, а не внутри формы и результатом API должен быть
success
илиfailed
action
BoryaMogila
02.11.2016 08:49-1failed action как раз и создаётся при ловле SubmissionError, а success action реализуется
// ошибок нет, обрабатываем данные data
Работа с API в данной статье необсуждалась.
При чем тут ваша команда?
n0ne
02.11.2016 10:41А мне тоже понравился, только я использую material-версию (-:
Мне нравится: просто, удобно, красиво (-:
Akuma
Может быть я что-то делаю не так, но я вообще не использую Redux для форм.
Формы — в 90% случаях обычные компоненты, которые не знают про окружение, т.к. оно им и не нужно. А значит, хватает обычного this.state.
Для зависимых форм данные либо подтягиваются снаружи, либо уж через connect() и props. Но Формы никогда не вызывают изменения Redux-store напрямую.
BoryaMogila
Это удобно и расширяемо. Все формы в одном месте, всегда есть доступ к ним извне, структурирование данных.
Удобно использовать на формах каталогов.
VasilioRuzanni
Ну тут считайте, что формы просто используют Redux как хранилище состояния, вместо хранения его в компонентах. Там отдельный редьюсер, и эта часть state ничего не знает про остальные. С этой частью стейта обычно не работают напрямую (однако иногда это бывает удобно, например, вручную залить серверной валидации ошибки в стейт в обход того, что предлагает redux-form).