如何设置基于异步源的初始值,比如使用redux-form的ajax调用?

时间:2022-08-22 21:55:46

On the official pages and in the GitHub issues for redux-form there are more than one example of how to work with initialValues however I cannot find a single one that focuses on explaining how initialValues can be set in response to an asynchronous source.

在官方页面和关于redux-form的GitHub问题中,有不止一个关于如何使用initialValues的例子,但是我找不到一个例子来解释如何在响应异步源时设置initialvalue。

The main case that I have in mind is something like a simple CRUD application where a user is going to edit some entity that already exists. When the view is first opened and the redux-form component is mounted but before the component is rendered the initialValues must be set. Lets say that in this example that the data is loaded on demand when the component is first mounted and rendered for the first time. The examples show setting initialValues based on hard coded values or the redux store state but none that I can find focus on how to set the initialValues based on something async like a call to XHR or fetch.

我想到的主要例子是一个简单的CRUD应用程序,其中用户将编辑已经存在的某个实体。当视图第一次打开并挂载redux-form组件时,但是在组件呈现之前,必须设置initialValues。这些示例显示了基于硬编码值或redux存储状态的设置初始值,但没有一种方法可以集中于如何根据异步调用XHR或fetch来设置初始值。

I'm sure I'm just missing something fundamental so please point me in the right direction.

我肯定我漏掉了一些基本的东西所以请给我指出正确的方向。

References:

引用:

4 个解决方案

#1


8  

EDIT: Updated Solution from ReduxForm docs

编辑:ReduxForm文档中更新的解决方案

This is now [documented]((https://redux-form.com/7.3.0/examples/initializefromstate/) in the latest version of ReduxForm, and is much simpler than my previous answer.

这在最新版本的ReduxForm中是[有文档记录的](https://redux-form.com/7.3.0/examples/initializefromstate/),比我之前的答案简单得多。

The key is to connect your form component after decorating it with ReduxForm. Then you will be able to access the initialValues prop just like any other prop on your component.

关键是将您的表单组件与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)

I accomplished this by using the redux-form reducer plugin method.

我通过使用redux-form reducer插件方法实现了这一点。

The following demos fetching async data and pre-populating a user form with response.

下面的演示将获取异步数据并使用响应预填充用户表单。

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

Then in your root reducer where you include your redux-form reducer you would include your reducer plugin that overrides the forms values with the returned fetched data.

然后,在包含redux-form reducer的根还原剂中,您将包含使用返回数据重写表单值的还原剂插件。

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

Finally you include you combine your new formReducer with the other reducers in your app.

最后,你将你的新格式化程序和应用程序中的其他减速程序结合起来。

Note The following assumes that the fetched user object's keys match the names of the fields in the user form. If this is not the case you will need to perform an additional step on the data to map fields.

注意,下面假设获取的用户对象的键与用户表单中的字段名匹配。如果不是这样,您将需要对数据进行额外的步骤来映射字段。

#2


3  

By default, you may only initialize a form component once via initialValues. There are two methods to reinitialize the form component with new "pristine" values:

默认情况下,只能通过initialValues初始化一个表单组件一次。有两种方法可以用新的“原始”值重新初始化表单组件:

Pass a enableReinitialize prop or reduxForm() config parameter set to true to allow the form the reinitialize with new "pristine" values every time the initialValues prop changes. To keep dirty form values when it reinitializes, you can set keepDirtyOnReinitialize to true. By default, reinitializing the form replaces all dirty values with "pristine" values.

将enableReinitialize prop或reduxForm()配置参数设置为true,以便在每次initialValues prop更改时允许窗体以新的“原始”值重新初始化。要在重初始化时保持脏表单值,可以将keepDirtyOnReinitialize设置为true。默认情况下,重新初始化表单将所有脏值替换为“原始”值。

Dispatch the INITIALIZE action (using the action creator provided by redux-form).

分派初始化操作(使用redux-form提供的操作创建者)。

Referenced from : http://redux-form.com/6.1.1/examples/initializeFromState/

引用:http://redux-form.com/6.1.1/examples/initializeFromState/

#3


2  

Could you fire the dispatch on componentWillMount(), and set the state to loading.

能否在componentWillMount()上启动分派,并将状态设置为装入。

While it is loading, render a spinner for example and only when the request returns with the values, update the state, and then re-render the form with the values??

当它在加载时,例如渲染一个微调器,并且只有当请求返回值时,更新状态,然后用值重新呈现窗体?

#4


0  

While this method may not be the best solution, it works well enough for my needs:

虽然这个方法可能不是最好的解决方案,但它可以满足我的需要:

  • AJAX request to API on entry
  • 对API的AJAX请求
  • Initializes form with data when request has been fulfilled or displays a server error
  • 当请求完成或显示服务器错误时,使用数据初始化表单
  • Resetting form will still reset to initial seed data
  • 重置表单仍将重置为初始种子数据
  • Allows the form to be reused for other purposes (for example, a simple if statement could bypass setting initial values): Add Post and Edit Post or Add Comment and Edit Comment...etc.
  • 允许为其他目的重用表单(例如,一个简单的if语句可以绕过设置初始值):添加Post和编辑Post,添加评论和编辑评论……
  • Data is removed from Redux form on exit (no reason to store new data in Redux since it's being re-rendered by a Blog component)
  • 数据在退出时从Redux表单中删除(没有理由在Redux中存储新数据,因为它正在被Blog组件重新呈现)

Form.jsx:

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's 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:

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:

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

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;

#1


8  

EDIT: Updated Solution from ReduxForm docs

编辑:ReduxForm文档中更新的解决方案

This is now [documented]((https://redux-form.com/7.3.0/examples/initializefromstate/) in the latest version of ReduxForm, and is much simpler than my previous answer.

这在最新版本的ReduxForm中是[有文档记录的](https://redux-form.com/7.3.0/examples/initializefromstate/),比我之前的答案简单得多。

The key is to connect your form component after decorating it with ReduxForm. Then you will be able to access the initialValues prop just like any other prop on your component.

关键是将您的表单组件与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)

I accomplished this by using the redux-form reducer plugin method.

我通过使用redux-form reducer插件方法实现了这一点。

The following demos fetching async data and pre-populating a user form with response.

下面的演示将获取异步数据并使用响应预填充用户表单。

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

Then in your root reducer where you include your redux-form reducer you would include your reducer plugin that overrides the forms values with the returned fetched data.

然后,在包含redux-form reducer的根还原剂中,您将包含使用返回数据重写表单值的还原剂插件。

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

Finally you include you combine your new formReducer with the other reducers in your app.

最后,你将你的新格式化程序和应用程序中的其他减速程序结合起来。

Note The following assumes that the fetched user object's keys match the names of the fields in the user form. If this is not the case you will need to perform an additional step on the data to map fields.

注意,下面假设获取的用户对象的键与用户表单中的字段名匹配。如果不是这样,您将需要对数据进行额外的步骤来映射字段。

#2


3  

By default, you may only initialize a form component once via initialValues. There are two methods to reinitialize the form component with new "pristine" values:

默认情况下,只能通过initialValues初始化一个表单组件一次。有两种方法可以用新的“原始”值重新初始化表单组件:

Pass a enableReinitialize prop or reduxForm() config parameter set to true to allow the form the reinitialize with new "pristine" values every time the initialValues prop changes. To keep dirty form values when it reinitializes, you can set keepDirtyOnReinitialize to true. By default, reinitializing the form replaces all dirty values with "pristine" values.

将enableReinitialize prop或reduxForm()配置参数设置为true,以便在每次initialValues prop更改时允许窗体以新的“原始”值重新初始化。要在重初始化时保持脏表单值,可以将keepDirtyOnReinitialize设置为true。默认情况下,重新初始化表单将所有脏值替换为“原始”值。

Dispatch the INITIALIZE action (using the action creator provided by redux-form).

分派初始化操作(使用redux-form提供的操作创建者)。

Referenced from : http://redux-form.com/6.1.1/examples/initializeFromState/

引用:http://redux-form.com/6.1.1/examples/initializeFromState/

#3


2  

Could you fire the dispatch on componentWillMount(), and set the state to loading.

能否在componentWillMount()上启动分派,并将状态设置为装入。

While it is loading, render a spinner for example and only when the request returns with the values, update the state, and then re-render the form with the values??

当它在加载时,例如渲染一个微调器,并且只有当请求返回值时,更新状态,然后用值重新呈现窗体?

#4


0  

While this method may not be the best solution, it works well enough for my needs:

虽然这个方法可能不是最好的解决方案,但它可以满足我的需要:

  • AJAX request to API on entry
  • 对API的AJAX请求
  • Initializes form with data when request has been fulfilled or displays a server error
  • 当请求完成或显示服务器错误时,使用数据初始化表单
  • Resetting form will still reset to initial seed data
  • 重置表单仍将重置为初始种子数据
  • Allows the form to be reused for other purposes (for example, a simple if statement could bypass setting initial values): Add Post and Edit Post or Add Comment and Edit Comment...etc.
  • 允许为其他目的重用表单(例如,一个简单的if语句可以绕过设置初始值):添加Post和编辑Post,添加评论和编辑评论……
  • Data is removed from Redux form on exit (no reason to store new data in Redux since it's being re-rendered by a Blog component)
  • 数据在退出时从Redux表单中删除(没有理由在Redux中存储新数据,因为它正在被Blog组件重新呈现)

Form.jsx:

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's 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:

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:

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

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;