web-dev-qa-db-ja.com

redux-formを使用したajax呼び出しなどの非同期ソースに基づいてinitialValuesを設定する方法

公式ページと redux-form のGitHubの問題には、initialValuesを扱う方法の例が複数ありますただし、非同期ソースに応答してinitialValuesを設定する方法を説明することに焦点を当てた単一のものは見つかりません。

私が念頭に置いている主なケースは、ユーザーが既存のエンティティを編集する単純なCRUDアプリケーションのようなものです。ビューが最初に開かれ、redux-formコンポーネントがマウントされるが、コンポーネントがレンダリングされる前にinitialValuesを設定する必要があります。この例では、コンポーネントが最初にマウントされ、初めてレンダリングされるときに、データがオンデマンドでロードされるとしましょう。例では、ハードコーディングされた値またはreduxストアの状態に基づいてinitialValuesを設定していますが、XHRの呼び出しやフェッチなどの非同期に基づいてinitialValuesを設定する方法に焦点を当てることはできません。

基本的なものが欠けているだけだと思う​​ので、正しい方向に向けてください。

参照:

23
jpierson

編集:ReduxFormのドキュメントから更新されたソリューション

これは、ReduxFormの最新バージョンでは documented であり、以前の回答よりもはるかに簡単です。

重要なのは、ReduxFormで装飾した後、フォームコンポーネントをconnectすることです。次に、コンポーネントの他のプロップと同様に、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)

Redux-form reducer plugin メソッドを使用してこれを達成しました。

次のデモでは、非同期データをフェッチし、ユーザーフォームに応答を事前入力します。

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

最後に、新しいformReducerをアプリの他のレデューサーと組み合わせます。

注以下は、フェッチされたユーザーオブジェクトのキーがユーザーフォームのフィールドの名前と一致することを前提としています。そうでない場合は、データに追加の手順を実行してフィールドをマップする必要があります。

12
ryandrewjohnson

ComponentWillMount()でディスパッチを起動し、状態をロードに設定できますか。

ロード中に、たとえば、リクエストが値とともに返されたときにのみスピナーをレンダリングし、状態を更新してから、値を使用してフォームを再レンダリングしますか?

2
luanped

非同期ソースに基づいてinitialValuesを設定する方法に関する最小限の作業例を次に示します。
それは initialize アクション作成者を使用します。

initialValuesのすべての値を未定義にしないでください。そうしないと、 無限ループになります になります。

// import { Field, reduxForm, change, initialize } from 'redux-form';

async someAsyncMethod() {
  // fetch data from server
  await this.props.getProducts(),

  // this allows to get current values of props after promises and benefits code readability
  const { products } = this.props;

  const initialValues = { productsField: products };

  // set values as pristine to be able to detect changes
  this.props.dispatch(initialize(
    'myForm',
    initialValues,
  ));
}
1

この方法は最善の解決策ではないかもしれませんが、私のニーズには十分に機能します。

  • エントリ時のAPIへのAJAXリクエスト
  • 要求が満たされたときにデータでフォームを初期化するか、サーバーエラーを表示します
  • フォームをリセットしても、初期シードデータにリセットされます
  • フォームを他の目的に再利用できます(たとえば、単純なifステートメントで初期値の設定をバイパスできます):投稿の追加と投稿の編集、またはコメントの追加とコメントの編集...など.
  • データは終了時にReduxフォームから削除されます(ブログコンポーネントによって再レンダリングされるため、Reduxに新しいデータを保存する理由はありません)

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:

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;
0
Matt Carlotta