I'm using redux-form. I have an onSubmit and a checkStatus method which both make an Ajax call to the API back-end. If the result of the Ajax call is not 200, I want to throw a SubmissionError.
The SubmissionError works fine if I throw it inside of onSubmit (it correctly shows me an error message on the form). But if I throw it inside of checkStatus, then I get an error complaining about an handled exception:
Uncaught (in promise) SubmissionError {errors: {…}, message: "Submit
Validation Failed", name: "SubmissionError", stack: "SubmissionError:
Submit Validation Failed at ...) at <anonymous>"}
I did see this comment that makes is seem like you are only supposed to throw SubmissionError from within onSubmit():
Here is my code:
import * as React from "react";
import { reduxForm, touch, Form, SubmissionError } from 'redux-form';
import ReduxFormField from './components/ReduxFormField'
export interface FormProps {
handleSubmit: any;
error: any;
}
export class MyForm extends React.Component<FormProps, any> {
private _inputVal: any;
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.checkStatus = this.checkStatus.bind(this);
}
checkStatus() {
return fetch("/checkStatus", {
method: 'GET',
}).then(response => {
if (response.status === 200) {
// checkStatus was successful!
} else {
throw new SubmissionError({
_error: "There was an error."
});
}
});
}
onSubmit(event) {
return fetch("/submit", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({'key1': 'val1'})
}).then(response => {
if (response.status === 200) {
// submit was successful!
} else {
throw new SubmissionError({
_error: "There was an error."
});
}
});
}
render() {
return (
<form onSubmit={this.props.handleSubmit(this.onSubmit)}>
<div>
<fieldset>
<ReduxFormField
type="text" fieldName="myInputValue"
ref={(input) => {
this._inputVal = input;
}}/>
</fieldset>
<input type="submit" value="Submit"/>
<a onClick={() => {
this.checkStatus()
}
}>Check Status
</a>
</div>
</form>
)
}
}
export default reduxForm({
form: 'myForm'
})(MyForm);
I suppose this action creator should help
https://redux-form.com/7.2.0/docs/api/actioncreators.md/#-code-setsubmitfailed-form-string-fields-string-code-
setSubmitFailed(form:String, ...fields:String) #
Flips submitFailed flag true, removes submitSucceeded and submitting, marks all the fields passed in as touched, and if at least one field was passed flips anyTouched to true.
Another option if to create a formField ( I don't mean a visible field, but just inside redux-form) called 'checkStatus' being true by default. When your check fails, simply dispatch action called change onto this field and supply the reduxForm with a validator which fails if field if false
Related
novice user of redux-form here. I have a signin modal that has 2 different operations: login and register. The role (stored in component state) will be login by default, and the user will be able to click a button to change it to register.
Where I'm stuck, is that I want to pass that piece of state to the onSubmit() function, so that I can dispatch the correct actions depending on if the user is trying to login or register.
My thinking was that I could pass down this piece of state called signInType as a prop to the function. Of course, it is not working as I would have expected. I can pass in a prop via the reduxForm HOC, but from that function I cannot access the component's state.
Here are the relevant parts of my component to help understand what my end goal is here:
const [signInType, setSignInType] = useState('login')
const onSubmit = (data, dispatch, props) => {
console.log('props: ', props);
if (props.signInType === 'login') {
return (
api.post('/Login', data)
.then(json => {
const response = JSON.parse(json.d)
if (!response.userid) {
console.error(response.message)
dispatch(emailLoginFailure(response.message))
return response.message
}
LogRocket.identify(response.userid, {
email: data.email,
})
dispatch(emailLoginSuccess(response))
})
.catch(err => {
console.error(err)
dispatch(emailLoginFailure(err))
})
)
} else if (props.signInType === 'register') {
return (
api.post('/RegisterByEmail', {
email: data.email,
password: data.password,
utm_source: "Development",
utm_medium: "email",
utm_campaign: "Campaign Test",
utm_term: "N/A",
utm_content: "123",
utm_date: "2019-02-11 12:25:36"
})
.then(json => {
const response = JSON.parse(json.d)
if (!response.userid) {
console.error(response.message)
dispatch(emailRegisterFailure(response.message))
return response.message
}
// LogRocket.identify(response.userid, {
// email: data.email,
// })
dispatch(emailRegisterSuccess(response))
})
.catch(err => {
console.error("Unable to register email:", err)
})
)
} else {
console.error("error: No signin type?")
}
}
Thanks for the help :)
Such login/register flow I prefer handling it with different components, in order to respect and follow SRP.
Also, I'm not sure how do you organize your components, but here's how I deal with such a scenario:
Your Modal:
* It will be responsible only for rendering Login or Register forms.
const Modal = () => {
const [signInType, ] = useState('login')
const isLogin = signInType === 'login'
return <>
{ isLogin ? <LoginForm /> : <RegisterForm /> }
<button onClick={() => setSignInType(isLogin ? 'register' : 'login')}>
{ isLogin ? 'Sign up' : 'Sign in' }
</button>
</>
}
LoginForm:
* Now you can pass your login action to onSubmit prop. Login will be your presentation component, while LoginForm decorates Login with reduxForm HOC.
export default reduxForm({
form: 'login',
onSubmit: data => {}
})(Login)
RegisterForm:
* Here we follow the same idea as LoginForm.
export default reduxForm({
form: 'register',
onSubmit: data => {}
})(Register)
I am fetching data from a remote API in componentDidMount:
componentDidMount() {
this.props.fetchRemoteData('photos')
}
And then the received data is passed to my component props in mapStateToProps, using a selector to filter a specific object from the received array:
const mapStateToProps = (state, { params }) => {
const photoId = parseInt(params.photoId)
return {
singlePhoto: getSinglePhoto(state.filteredList.photos.jsonArray, photoId),
isFetching: state.filteredList.photos.isFetching
}
}
The content renders, but there is a split second before that, where it seems to be trying to the render the content before the data is successfully retrieved, which brings up the following error in the console:
Uncaught TypeError: Cannot read property 'charAt' of undefined
undefined is here referring to this.props.singlePhoto. But when singlePhoto receives the data payload the content renders.
Here is my container component:
class PhotoSingle extends Component {
componentDidMount() {
this.props.fetchRemoteData('photos')
}
render() {
const {singlePhoto, isFetching} = this.props
const photoTitle = capitalizeFirstLetter(singlePhoto.title)
return (
<div>
<PhotoSingleImg singlePhoto={singlePhoto} photoTitle={photoTitle} isFetching={isFetching}/>
</div>
)
}
}
const mapStateToProps = (state, { params }) => {
const photoId = parseInt(params.photoId)
return {
singlePhoto: getSinglePhoto(state.filteredList.photos.jsonArray, photoId),
isFetching: state.filteredList.photos.isFetching
}
}
import * as actions from '../actions/actionCreators'
PhotoSingle = connect(mapStateToProps, actions)(PhotoSingle)
export default PhotoSingle;
And my presentational component:
const PhotoSingleImg = ({ singlePhoto, photoTitle, isFetching }) => {
if (isFetching) {
return <h4>Fetching data...</h4>
}
return (
<div>
<h1>Single Photo</h1>
<h3>Title</h3>
<hr />
<img className='single-photo' src={singlePhoto.url} />
<p>Album ID: {singlePhoto.albumId} | Photo ID: {singlePhoto.id}</p>
</div>
)
}
export default PhotoSingleImg;
I'm unsure how to make it so the content will only attempt to render after I the API response has been received.
Any help appreciated.
Have you defined initial state in redux store?
You can try this way:
return singlePhoto ?
(<div>
<h1>Single Photo</h1>
<h3>Title</h3>
<hr />
<img className='single-photo' src={singlePhoto.url} />
<p>Album ID: {singlePhoto.albumId} | Photo ID: {singlePhoto.id}</p>
</div>) : null
I am looking for an elegant way to display validation messages from a server side API without having to create custom validators or hard coding all possible messages in the UI.
I need to add error messages to specific fields as well as to the entire form.
This must work in Angular 2.0.0-beta.3
There are two kinds of server validations:
The global ones (for the whole form or corresponding to an error during the form submission)
The ones related to fields
For the one, simply extract the message from the response payload and put it into a property of your component to display it into the associated template:
#Component({
(...)
template: `
<form (submit)="onSubmit()">
(...)
<div *ngIf="errorMessage">{{errorMessage}}</div>
<button type="submit">Submit</button>
</form>
`
})
export class MyComponent {
(...)
onSubmit() {
this.http.post('http://...', data)
.map(res => res.json())
.subscribe(
(data) => {
// Success callback
},
(errorData) => {
// Error callback
var error = errorData.json();
this.error = `${error.reasonPhrase} (${error.code})`;
}
);
}
}
I assume that the response payload for error is a JSON one and corresponds to the following:
{
"code": 422,
"description": "Some description",
"reasonPhrase": "Unprocessable Entity"
}
For the second one, you can set received error message within controls associated with form inputs, as described below:
#Component({
(...)
template: `
<form [ngFormModel]="myForm" (submit)="onSubmit()">
(...)
Name: <input [ngFormControl]="myForm.controls.name"/>
<span *ngIf="myForm.controls.name.errors?.remote"></span>
(...)
<button type="submit">Submit</button>
</form>
`
})
export class MyComponent {
(...)
constructor(builder:FormBuilder) {
this.myForm = this.companyForm = builder.group({
name: ['', Validators.required ]
});
}
onSubmit() {
this.http.post('http://...', data)
.map(res => res.json())
.subscribe(
(data) => {
// Success callback
},
(errorData) => {
// Error callback
var error = errorData.json();
var messages = error.messages;
messages.forEach((message) => {
this.companyForm.controls[message.property].setErrors({
remote: message.message });
});
}
);
}
}
I assume that the response payload for error is a JSON one and corresponds to the following:
{
messages: [
{
"property": "name",
"message": "The value can't be empty"
]
}
For more details you can have a look at this project:
https://github.com/restlet/restlet-sample-angular2-forms/blob/master/src/app/components/company.details.ts
https://github.com/restlet/restlet-sample-angular2-forms/blob/master/src/app/components/form.field.ts
I present you the definitive displayErrors function (Handles server side validations following the JSONAPI Standard):
You will need Underscore.js
displayErrors(error: ErrorResponse) {
let controls = this.supportRequestForm.controls;
let grouped = _.groupBy(error['errors'], function(e) {
return e['source']['pointer'];
});
_.each(grouped, function(value, key, object) {
let attribute = key.split('/').pop();
let details = _.map(value, function(item) { return item['detail']; });
controls[attribute].setErrors({ remote: details.join(', ') });
});
}
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.
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.
I'm sure I'm just missing something fundamental so please point me in the right direction.
References:
Initializing Form State
Handling form defaults
What is the correct way to populate a dynamic form with initial data?
EDIT: Updated Solution from ReduxForm docs
This is now documented in the latest version of ReduxForm, and is much simpler than my previous answer.
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.
// 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.
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.
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.
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:
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.
Dispatch the INITIALIZE action (using the action creator provided by redux-form).
Referenced from : http://redux-form.com/6.1.1/examples/initializeFromState/
Could you fire the dispatch on componentWillMount(), and set the state to loading.
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??
Here is minimal working example on how to set initialValues based on async source.
It uses initialize action creator.
All values from initialValues shouldn't be undefined, or you will get an infinite loop.
// 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,
));
}
While this method may not be the best solution, it works well enough for my needs:
AJAX request to API on entry
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.
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)
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;
use this :
UpdateUserForm = reduxForm({
enableReinitialize: true,
destroyOnUnmount: false,
form: 'update_user_form' // a unique identifier for this form
})(UpdateUserForm);
UpdateUserForm = connect(
(state) => ({
initialValues: state.userManagment.userSingle
})
)(UpdateUserForm);
export default UpdateUserForm;
How to make server-side validation using redux-form and Fetch API?
There are "Submit Validation" demo provided in the docs which says that recommended way to do server side validation is to return a promise from the onSubmit function. But where should I place that promise?
As I understood onSubmit function should be my action.
<form onSubmit={this.props.addWidget}>...
Where this.props.addWidget is actually my action, provided below.
import fetch from 'isomorphic-fetch';
...
function fetchAddWidget(widget, workspace) {
return dispatch => {
dispatch(requestAddWidget(widget, workspace));
return fetch.post(`/service/workspace/${workspace}/widget`, widget)
.then(parseJSON)
.then(json => {
dispatch(successAddWidget(json, workspace));
DataManager.handleSubscribes(json);
})
.catch(error => popupErrorMessages(error));
}
}
export function addWidget(data, workspace) {
return (dispatch, getState) => {
return dispatch(fetchAddWidget(data, workspace));
}
}
As you see I use fetch API. I expected that fetch will return promise and redux-form will catch it but that doesn't work. How to make it work with promise from example?
Also from the demo I can not understand what should be provided in this.props.handleSubmit function. Demo does not explain this part, as for me.
Here's my take on using fetch based on the example at http://erikras.github.io/redux-form/#/examples/submit-validation.
...but where should I place that promise?
...what should be provided in this.props.handleSubmit?
The detail is in the comments below; sorry that the code blocks require a bit of scrolling to read :/
components/submitValidation.js
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { myHandleSubmit, show as showResults } from '../redux/modules/submission';
class SubmitValidationForm extends Component {
// the following three props are all provided by the reduxForm() wrapper / decorator
static propTypes = {
// the field names we passed in the wrapper;
// each field is now an object with properties:
// value, error, touched, dirty, etc
// and methods onFocus, onBlur, etc
fields: PropTypes.object.isRequired,
// handleSubmit is _how_ to handle submission:
// eg, preventDefault, validate, etc
// not _what_ constitutes or follows success or fail.. that's up to us
// I must pass a submit function to this form, but I can either:
// a) import or define a function in this component (see above), then:
// `<form onSubmit={ this.props.handleSubmit(myHandleSubmit) }>`, or
// b) pass that function to this component as
// `<SubmitValidationForm onSubmit={ myHandleSubmit } etc />`, then
// `<form onSubmit={this.props.handleSubmit}>`
handleSubmit: PropTypes.func.isRequired,
// redux-form listens for `reject({_error: 'my error'})`, we receive `this.props.error`
error: PropTypes.string
};
render() {
const { fields: { username, password }, error, handleSubmit } = this.props;
return (
<form onSubmit={ handleSubmit(myHandleSubmit) }>
<input type="text" {...username} />
{
// this can be read as "if touched and error, then render div"
username.touched && username.error && <div className="form-error">{ username.error }</div>
}
<input type="password" {...password} />
{ password.touched && password.error && <div className="form-error">{ password.error }</div> }
{
// this is the generic error, passed through as { _error: 'something wrong' }
error && <div className="text-center text-danger">{ error }</div>
}
// not sure why in the example #erikras uses
// `onClick={ handleSubmit }` here.. I suspect a typo.
// because I'm using `type="submit"` this button will trigger onSubmit
<button type="submit">Log In</button>
</form>
);
}
}
// this is the Higher Order Component I've been referring to
// as the wrapper, and it may also be written as a #decorator
export default reduxForm({
form: 'submitValidation',
fields: ['username', 'password'] // we send only field names here
})(SubmitValidationForm);
../redux/modules/submission.js
// (assume appropriate imports)
function postToApi(values) {
return fetch( API_ENDPOINT, {
credentials: 'include',
mode: 'cors',
method: 'post',
body: JSON.stringify({values}),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': CSRF_TOKEN
}
}).then( response => Promise.all([ response, response.json()] ));
}
export const myHandleSubmit = (values, dispatch) => {
dispatch(startLoading());
return new Promise((resolve, reject) => {
// postToApi is a wrapper around fetch
postToApi(values)
.then(([ response, json ]) => {
dispatch(stopLoading());
// your statuses may be different, I only care about 202 and 400
if (response.status === 202) {
dispatch(showResults(values));
resolve();
}
else if (response.status === 400) {
// here I expect that the server will return the shape:
// {
// username: 'User does not exist',
// password: 'Wrong password',
// _error: 'Login failed!'
// }
reject(json.errors);
}
else {
// we're not sure what happened, but handle it:
// our Error will get passed straight to `.catch()`
throw(new Error('Something went horribly wrong!'));
}
})
.catch( error => {
// Otherwise unhandled server error
dispatch(stopLoading());
reject({ _error: error });
});
});
};
Please chime in with comments if I've missed something / misinterpreted, etc, and I'll amend :)
It turned out that there are undocumented property returnRejectedSubmitPromise which must be set to true.