Как установить initialValues на основе async-источника, такого как ajax-вызов с формой redux
На официальных страницах и в проблемах GitHub для redux-form есть несколько примеров того, как работать с initialValues, но я не могу найдите один, который фокусируется на объяснении того, как initialValues могут быть установлены в ответ на асинхронный источник.
Основной случай, который я имею в виду, - это что-то вроде простого приложения CRUD, в котором пользователь собирается редактировать некоторую сущность, которая уже существует. Когда открывается первое представление и компонент редукционной формы монтируется, но перед визуализацией компонента должны быть установлены начальные значения. Допустим, что в этом примере данные загружаются по требованию, когда компонент впервые установлен и отображен в первый раз. Примеры показывают установку initialValues на основе жестко закодированных значений или состояния хранилища redux, но я не могу сосредоточиться на том, как установить начальные значения на основе чего-то async, как вызов XHR или выборки.
Я уверен, что я просто пропустил что-то фундаментальное, поэтому, пожалуйста, укажите мне в правильном направлении.
Литература:
Ответы
Ответ 1
EDIT: Обновлено решение из документов ReduxForm
Теперь это [документально] ((http://redux-form.com/6.0.0-alpha.4/examples/initializeFromState/) в последней версии ReduxForm и намного проще, чем мой предыдущий ответ.
Ключ к connect
компоненту формы после его оформления с помощью ReduxForm. Затем вы сможете получить доступ к опоре initialValues
, как и любая другая поддержка вашего компонента.
// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
InitializeFromStateForm = reduxForm({
form: 'initializeFromState'
})(InitializeFromStateForm)
// now set initialValues using data from your store state
InitializeFromStateForm = connect(
state => ({
initialValues: state.account.data
})
)(InitializeFromStateForm)
Я выполнил это, используя метод редукции редуктор.
Следующие демонстрации извлекают асинхронные данные и предварительно заполняют форму пользователя с ответом.
const RECEIVE_USER = 'RECEIVE_USER';
// once you've received data from api dispatch action
const receiveUser = (user) => {
return {
type: RECEIVE_USER,
payload: { user }
}
}
// here is your async request to retrieve user data
const fetchUser = (id) => dispatch => {
return fetch('http://getuser.api')
.then(response => response.json())
.then(json => receiveUser(json));
}
Затем в корневом редукторе, где вы включаете редуктор redux-form
, вы должны включить плагин редуктора, который переопределяет значения форм с возвращенными данными.
const formPluginReducer = {
form: formReducer.plugin({
// this would be the name of the form you're trying to populate
user: (state, action) => {
switch (action.type) {
case RECEIVE_USER:
return {
...state,
values: {
...state.values,
...action.payload.user
}
}
default:
return state;
}
}
})
};
const rootReducer = combineReducers({
...formPluginReducer,
...yourOtherReducers
});
Наконец, вы включаете в себя объединение своей новой формыReducer с другими редукторами в вашем приложении.
Примечание. Предполагается, что выбранные ключи объектов пользователя соответствуют именам полей в пользовательской форме. Если это не так, вам нужно будет выполнить дополнительный шаг по данным для отображения полей.
Ответ 2
По умолчанию вы можете инициализировать компонент формы только через initialValues. Существует два способа повторной инициализации компонента формы с новыми "нетронутыми" значениями:
Передайте параметр enableReinitialize prop или reduxForm(), установленный в true, чтобы позволить форме повторно инициализировать с новыми значениями "нетронутых" каждый раз, когда изменяется начальная поддержка. Чтобы сохранить значения грязной формы при повторной инициализации, вы можете установить для keepDirtyOnReinitialize значение true. По умолчанию повторная инициализация формы заменяет все грязные значения "нетронутыми" значениями.
Отправляйте действие INITIALIZE (используя создателя действия, предоставленного редукционной формой).
Ссылка на: http://redux-form.com/6.1.1/examples/initializeFromState/
Ответ 3
Не удалось запустить диспетчер компонента componentWillMount() и установить состояние для загрузки.
Пока он загружается, отрисуйте spinner, например, и только тогда, когда запрос возвращается со значениями, обновите состояние, а затем повторно отобразите форму со значениями
Ответ 4
Хотя этот метод может быть не лучшим решением, он работает достаточно хорошо для моих нужд:
- Запрос AJAX для API при входе
- Инициализирует форму с данными, когда запрос был выполнен или отображает ошибку сервера.
- Сброс формы по-прежнему будет reset к исходным данным семени
- Позволяет повторно использовать форму для других целей (например, простой оператор if может обходить установки начальных значений): Добавить сообщение и изменить сообщение или добавить комментарий и изменить комментарий... и т.д.
- Данные удаляются из формы Redux при выходе (нет причины хранить новые данные в Redux, поскольку они повторно отображаются компонентом Blog).
Form.jsx:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router';
import { editPost, fetchPost } from '../../actions/BlogActions.jsx';
import NotFound from '../../components/presentational/notfound/NotFound.jsx';
import RenderAlert from '../../components/presentational/app/RenderAlert.jsx';
import Spinner from '../../components/presentational/loaders/Spinner.jsx';
// form validation checks
const validate = (values) => {
const errors = {}
if (!values.title) {
errors.title = 'Required';
}
if (!values.image) {
errors.image = 'Required';
}
if (!values.description) {
errors.description = 'Required';
} else if (values.description.length > 10000) {
errors.description = 'Error! Must be 10,000 characters or less!';
}
return errors;
}
// renders input fields
const renderInputField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<input {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers "><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
// renders a text area field
const renderAreaField = ({ textarea, input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<textarea {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers"><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
class BlogPostForm extends Component {
constructor() {
super();
this.state = {
isLoaded: false,
requestTimeout: false,
};
}
componentDidMount() {
if (this.props.location.query.postId) {
// sets a 5 second server timeout
this.timeout = setInterval(this.timer.bind(this), 5000);
// AJAX request to API
fetchPost(this.props.location.query.postId).then((res) => {
// if data returned, seed Redux form
if (res.foundPost) this.initializeForm(res.foundPost);
// if data present, set isLoaded to true, otherwise set a server error
this.setState({
isLoaded: (res.foundPost) ? true : false,
serverError: (res.err) ? res.err : ''
});
});
}
}
componentWillUnmount() {
this.clearTimeout();
}
timer() {
this.setState({ requestTimeout: true });
this.clearTimeout();
}
clearTimeout() {
clearInterval(this.timeout);
}
// initialize Redux form from API supplied data
initializeForm(foundPost) {
const initData = {
id: foundPost._id,
title: foundPost.title,
image: foundPost.image,
imgtitle: foundPost.imgtitle,
description: foundPost.description
}
this.props.initialize(initData);
}
// onSubmit => take Redux form props and send back to server
handleFormSubmit(formProps) {
editPost(formProps).then((res) => {
if (res.err) {
this.setState({
serverError: res.err
});
} else {
browserHistory.push(/blog);
}
});
}
renderServerError() {
const { serverError } = this.state;
// if form submission returns a server error, display the error
if (serverError) return <RenderAlert errorMessage={serverError} />
}
render() {
const { handleSubmit, pristine, reset, submitting, fields: { title, image, imgtitle, description } } = this.props;
const { isLoaded, requestTimeout, serverError } = this.state;
// if data hasn't returned from AJAX request, then render a spinner
if (this.props.location.query.postId && !isLoaded) {
// if AJAX request returns an error or request has timed out, show NotFound component
if (serverError || requestTimeout) return <NotFound />
return <Spinner />
}
// if above conditions are met, clear the timeout, otherwise it'll cause the component to re-render on timer setState function
this.clearTimeout();
return (
<div className="col-sm-12">
<div className="form-container">
<h1>Edit Form</h1>
<hr />
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<Field name="title" type="text" component={renderInputField} label="Post Title" />
<Field name="image" type="text" component={renderInputField} label="Image URL" />
<Field name="imgtitle" component={renderInputField} label="Image Description" />
<Field name="description" component={renderAreaField} label="Description" />
<div>
<button type="submit" className="btn btn-primary partial-expand rounded" disabled={submitting}>Submit</button>
<button type="button" className="btn btn-danger partial-expand rounded f-r" disabled={ pristine || submitting } onClick={ reset }>Clear Values</button>
</div>
</form>
{ this.renderServerError() }
</div>
</div>
)
}
}
BlogPostForm = reduxForm({
form: 'BlogPostForm',
validate,
fields: ['name', 'image', 'imgtitle', 'description']
})(BlogPostForm);
export default BlogPostForm = connect(BlogPostForm);
BlogActions.jsx:
import * as app from 'axios';
const ROOT_URL = 'http://localhost:3001';
// submits Redux form data to server
export const editPost = ({ id, title, image, imgtitle, description, navTitle }) => {
return app.put(`${ROOT_URL}/post/edit/${id}?userId=${config.user}`, { id, title, image, imgtitle, description, navTitle }, config)
.then(response => {
return { success: response.data.message }
})
.catch(({ response }) => {
if(response.data.deniedAccess) {
return { err: response.data.deniedAccess }
} else {
return { err: response.data.err }
}
});
}
// fetches a single post from the server for front-end editing
export const fetchPost = (id) => {
return app.get(`${ROOT_URL}/posts/${id}`)
.then(response => {
return { foundPost: response.data.post}
})
.catch(({ response }) => {
return { err: response.data.err };
});
}
RenderAlert.jsx:
import React, { Component } from 'react';
const RenderAlert = (props) => {
const displayMessage = () => {
const { errorMessage } = props;
if (errorMessage) {
return (
<div className="callout-alert">
<p>
<i className="fa fa-exclamation-triangle" aria-hidden="true"/>
<strong>Error! </strong> { errorMessage }
</p>
</div>
);
}
}
return (
<div>
{ displayMessage() }
</div>
);
}
export default RenderAlert;
Reducers.jsx
import { routerReducer as routing } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
form: formReducer,
routing
});
export default rootReducer;