Using Redux-forms how do I supply a callback to execute after successful async form submission? - redux-form

I have a redux form that will call a rest API on form submission. If the API call fails I'd like to raise a SubmissionError so that the error can be displayed on the form. As the submission error details map to properties on the form I would like this handling to be part of the form component.
Once the form has successfully submitted to the API and got a success response I would like the form component to call a method (methodA) supplied to the form component.
At the moment the only way I can see of doing this is to:
Pass methodA in the props of the form component.
In the form component's call to reduxForm() supply a method to onSubmitSuccess in the config object (methodB)
In methodB pull methodA from the supplied props and then call it
Is this the best way of doing what I want or is there a simpler way?

In the action creator (assuming you are using react-thunk)
const submit = (url, data) => dispatch => {
dispatch({ type: 'submit-start' });
return new Promise((res, rej) =>
fetch(/*do some stuff*/)
.then(res => {
if (res.status !== 200) {
dispatch({ type: 'submit-err' })
//the object which is rejected here
//configures the errors displayed in the form
//in best case your server delivers an appropriate
//response
rej({ _error: 'Validation Failed', age: 'too young' });
} else {
dispatch({ type: 'submit-success' });
res(res.json())
}
})
);
}
export { submit };
and in the component:
submit (data) {
return this.props.submit(<url>, data)
}
render () {
const { handleSubmit } = this.props;
return <form onSubmit={handleSubmit(submit)}>…</form>
}
If the returned Promise from the action creator is rejected, the error message is displayed.
Redux-Thunk returns the return value of the function called with dispatch as parameter, in this case a promise.
For redux form submit validation, the submit function must return a promise, which can be resolved, or rejected.
docs

Related

How to redirect after fullfield response? [duplicate]

I have a situation where I should get a song item by id to get the path for that song, and then navigate to that song on button click.
Is there any specific hook that can be used to navigate on data arrival, useEffect will be called any time that state changes but the problem is that first needs to be dispatched the action to get the song, check if it returns any item and then navigate. Typically if it is has been published on the list, it should exist on the db, but the problem might be at the API side, so that check results.length > 0 is why that check is necessary.
useEffect(() => {
const handleClick = (myId: string) => {
dispatch(SongActions.searchSong(myId));
if (results.length > 0) {
if (Object.keys(results[0]).length > 0) {
// navigate(`/songs/${results[0].myPath}`);
}
}
}
}, [dispatch, results])
When user clicks on list item which has a song title, it should call the function handleClick(id) with id of the song as parameter, that is to get the metadata of the song, src path etc.
<Typography onClick={() => handleClick(songItem.songId)} sx={styles.songListItemText}>{songItem.Title}</Typography>
Edit
And this is how I have setup the searchSong action:
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch({
payload: { isLoading: true },
type: SearchActionType.REQUEST,
});
try {
const response = await SearchApi.searchSongAsync(obj);
if (response.length === 0) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch({
type: SearchActionType.RECEIVED_SONG,
payload: { results: response },
});
} catch (e) {
console.error("Error: ", e);
}
}
You appear to be mixing up the purpose of the useEffect hook and asynchronous event handlers like button element's onClick handlers. The useEffect hook is to meant to issue intentional side-effects in response to some dependency value updating and is tied to the React component lifecycle, while onClick handlers/etc are meant to respond to asynchronous events, i.e. a user clicking a button. They don't mix.
Assuming SongActions.searchSong is an asynchronous action, you've correctly setup Redux middleware to handle them (i.e. Thunks), and the action returns the fetched response data, then the dispatched action returns a Promise that the callback can wait for.
Example:
const navigate = useNavigate();
const dispatch = useDispatch();
const handleClick = async (myId: string) => {
const results = await dispatch(SongActions.searchSong(myId));
if (results.length > 0 && Object.keys(results[0]).length > 0) {
navigate(`/songs/${results[0].myPath}`);
}
};
...
<Typography
onClick={() => handleClick(songItem.songId)}
sx={styles.songListItemText}
>
{songItem.Title}
</Typography>
The searchSong action creator should return a resolved value for consumers to await for.
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch(startRequest());
try {
const results = await SearchApi.searchSongAsync(obj);
if (!results.length) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch(receivedSong({ results }));
return results; // <-- return resolved value here
} catch (e) {
console.error("Error: ", e);
} finally {
dispatch(completeRequest());
}
}
You can create a state such as const [isDataPresent, setIsDataPresent] = useState(false) to keep track of if the data has arrived or not. And as David has mentioned in the comments you cannot call the function inside the useEffect on handleClick. Instead what you can do is create that function outside the useEffect hook and inside the same function you fetch the data and check if the data is at all present, if present then you can set the above boolean state to true and then redirect from that function itself.
Since you are already fetching the data from the same API and different endpoint, what you can do is -
Create a new component.
Since you are mapping over the data send the data to this component by rendering it inside the map function. It'd allow the data to be passed and components to be rendered one by one.
Create a state in the new component.
Use useEffect hook to fetch the data for a single song since when you are passing the data from the previous component to this one you would also get access to the ID and store it inside the state. This would be occurring inside the newly created component.

How to test emit event on ngSubmit within component subscribe method

On form submit getUsers() is called and if success message is received, the received data is emitted to the parent component.
child html
<form (ngSubmit)="getUsers()">
</form>
child component
getUsers(): void {
this.userService.getUsers().subscribe(users => {
if(users.status=="Success"{
this.listOfUsers = users;
this.user.emit(this.listOfUsers);
this.nextTab.emit(true);
}
});
}
i have written the test case to check emit event as follows
it('should emit data on success', () => {
spyOn(component.user,'emit');
component.getUsers();
expect(component.user.emit).toHaveBeenCalled(); // fails as never called what i am doing wrong
});
You have to make sure that userService.getUsers returns an observable.
import { of } from 'rxjs';
....
it('should emit data on success', () => {
// mock userService.getUsers to return { status: 'Success' } to go inside of the if block
spyOn(userService, 'getUsers').and.returnValue(of({ status: 'Success' }));
spyOn(component.user,'emit');
component.getUsers();
expect(component.user.emit).toHaveBeenCalled(); // fails as never called what i am doing wrong
});
Edit
To actually test ngSubmit, I would use triggerEventHandler. You can do some research on it.
const form = fixture.debugElement.query(By.css('form'));
// The 2nd argument is what you would like the $event value to be.
// In our case, null is fine.
form.triggerEventHandler('ngSubmit', null);
Doing the above will call getUsers.

custom async validation not working when returning a promise

I'm calling the web api to check if an urlalias is available, for this task I'm using a httpservice in my async validator.
The issue is that when the validator is called, all the correct code path is performed (all the console.log() run and behave as expected).
Whether the promise from the validation returns/resolves to null or an { 'isUrlAliasActivityMainAvailable': true }, the controller always shows an error object as following, thus keeping the form state as invalid, why (bloody hell!)?
I'm using: angular:2.1.0 and rxjs:5.0.0-beta.12
This is my formbuilder:
this.formBuilder.group({
//...
"urlAliasActivityMain":[null,[ ValidatorsZr.isUrlAliasActivityMainAvailableAsyncValidator(this.httpActivityService)]],
});
This is my validator:
public static isUrlAliasActivityMainAvailableAsyncValidator(httpActivityService: HttpActivityService) {
return function (control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>(
(resolve, reject) => {
httpActivityService.isUrlAliasActivityMainAvailable(control.value)
.subscribe(
(data: any) => {
console.log("isUrlAliasActivityMainAvailableAsyncValidator");
console.log(data);
if (data == false) {
console.log("data == false");
resolve({ 'isUrlAliasActivityMainAvailable': true });
}
else {
console.log("data == true");
resolve(null);
}
},
)
});
return promise;
}
}
Your async validator is listed in the synchronous validators location in the array and is being incorrectly evaluated.
[objectValue, synchronous validators, asynchronous validators]
control(formState: Object, validator?: ValidatorFn|ValidatorFn[],
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]) : FormControl
Construct a new FormControl with the given formState,validator, and
asyncValidator.
formState can either be a standalone value for the form control or an
object that contains both a value and a disabled status.
To correct it, move your validator to the appropriate array location:
this.formBuilder.group({
//...
"urlAliasActivityMain":[null, null, ValidatorsZr.isUrlAliasActivityMainAvailableAsyncValidator(this.httpActivityService)],
});

Returning Promises from Vuex actions

I recently started migrating things from jQ to a more structured framework being VueJS, and I love it!
Conceptually, Vuex has been a bit of a paradigm shift for me, but I'm confident I know what its all about now, and totally get it! But there exist a few little grey areas, mostly from an implementation standpoint.
This one I feel is good by design, but don't know if it contradicts the Vuex cycle of uni-directional data flow.
Basically, is it considered good practice to return a promise(-like) object from an action? I treat these as async wrappers, with states of failure and the like, so seems like a good fit to return a promise. Contrarily mutators just change things, and are the pure structures within a store/module.
actions in Vuex are asynchronous. The only way to let the calling function (initiator of action) to know that an action is complete - is by returning a Promise and resolving it later.
Here is an example: myAction returns a Promise, makes a http call and resolves or rejects the Promise later - all asynchronously
actions: {
myAction(context, data) {
return new Promise((resolve, reject) => {
// Do something here... lets say, a http call using vue-resource
this.$http("/api/something").then(response => {
// http success, call the mutator and change something in state
resolve(response); // Let the calling function know that http is done. You may send some data back
}, error => {
// http failed, let the calling function know that action did not work out
reject(error);
})
})
}
}
Now, when your Vue component initiates myAction, it will get this Promise object and can know whether it succeeded or not. Here is some sample code for the Vue component:
export default {
mounted: function() {
// This component just got created. Lets fetch some data here using an action
this.$store.dispatch("myAction").then(response => {
console.log("Got some data, now lets show something in this component")
}, error => {
console.error("Got nothing from server. Prompt user to check internet connection and try again")
})
}
}
As you can see above, it is highly beneficial for actions to return a Promise. Otherwise there is no way for the action initiator to know what is happening and when things are stable enough to show something on the user interface.
And a last note regarding mutators - as you rightly pointed out, they are synchronous. They change stuff in the state, and are usually called from actions. There is no need to mix Promises with mutators, as the actions handle that part.
Edit: My views on the Vuex cycle of uni-directional data flow:
If you access data like this.$store.state["your data key"] in your components, then the data flow is uni-directional.
The promise from action is only to let the component know that action is complete.
The component may either take data from promise resolve function in the above example (not uni-directional, therefore not recommended), or directly from $store.state["your data key"] which is unidirectional and follows the vuex data lifecycle.
The above paragraph assumes your mutator uses Vue.set(state, "your data key", http_data), once the http call is completed in your action.
Just for an information on a closed topic:
you don’t have to create a promise, axios returns one itself:
Ref: https://forum.vuejs.org/t/how-to-resolve-a-promise-object-in-a-vuex-action-and-redirect-to-another-route/18254/4
Example:
export const loginForm = ({ commit }, data) => {
return axios
.post('http://localhost:8000/api/login', data)
.then((response) => {
commit('logUserIn', response.data);
})
.catch((error) => {
commit('unAuthorisedUser', { error:error.response.data });
})
}
Another example:
addEmployee({ commit, state }) {
return insertEmployee(state.employee)
.then(result => {
commit('setEmployee', result.data);
return result.data; // resolve
})
.catch(err => {
throw err.response.data; // reject
})
}
Another example with async-await
async getUser({ commit }) {
try {
const currentUser = await axios.get('/user/current')
commit('setUser', currentUser)
return currentUser
} catch (err) {
commit('setUser', null)
throw 'Unable to fetch current user'
}
},
Actions
ADD_PRODUCT : (context,product) => {
return Axios.post(uri, product).then((response) => {
if (response.status === 'success') {
context.commit('SET_PRODUCT',response.data.data)
}
return response.data
});
});
Component
this.$store.dispatch('ADD_PRODUCT',data).then((res) => {
if (res.status === 'success') {
// write your success actions here....
} else {
// write your error actions here...
}
})
TL:DR; return promises from you actions only when necessary, but DRY chaining the same actions.
For a long time I also though that returning actions contradicts the Vuex cycle of uni-directional data flow.
But, there are EDGE CASES where returning a promise from your actions might be "necessary".
Imagine a situation where an action can be triggered from 2 different components, and each handles the failure case differently.
In that case, one would need to pass the caller component as a parameter to set different flags in the store.
Dumb example
Page where the user can edit the username in navbar and in /profile page (which contains the navbar). Both trigger an action "change username", which is asynchronous.
If the promise fails, the page should only display an error in the component the user was trying to change the username from.
Of course it is a dumb example, but I don't see a way to solve this issue without duplicating code and making the same call in 2 different actions.
actions.js
const axios = require('axios');
const types = require('./types');
export const actions = {
GET_CONTENT({commit}){
axios.get(`${URL}`)
.then(doc =>{
const content = doc.data;
commit(types.SET_CONTENT , content);
setTimeout(() =>{
commit(types.IS_LOADING , false);
} , 1000);
}).catch(err =>{
console.log(err);
});
},
}
home.vue
<script>
import {value , onCreated} from "vue-function-api";
import {useState, useStore} from "#u3u/vue-hooks";
export default {
name: 'home',
setup(){
const store = useStore();
const state = {
...useState(["content" , "isLoading"])
};
onCreated(() =>{
store.value.dispatch("GET_CONTENT" );
});
return{
...state,
}
}
};
</script>

React/redux app function not firing

Update 2:
Another update - I'm down to this one issue. The values from redux-form are being sent to the action, but even narrowed down to this:
export function signinUser(values) {
console.log('function about to run, email: ' values.get('email'));
}
I don't even see a console log entry. However, the same function with only a simple console log works:
export function signinUser() {
console.log('function about to run');
}
Update:
The only differences between the working code and non-working code is redux-form and immutablejs. I tried back-porting these to the working app and now the behaviour is the same.
I'm submitting my form and sending the redux-form values to the function, where I'm using values.get('email') and values.get('password') to pass the values to axios.
handleFormSubmit(values) {
console.log('sending to action...', values);
this.props.signinUser(values);
}
I have a login form and onSubmit I'm passing the values to a function which dispatches actions.
The code works in a repo I've forked, but I'm transferring it to my own app. The problem I'm having is that the function doesn't seem to fire, and I'm struggling to figure out where to add console.log statements.
The only console.log that fires is on line 2.
export function signinUser(values) {
console.log('function will run');
return function(dispatch) {
axios.post(TOKEN_URL, {
email: "a#a.com",
password: "a",
method: 'post',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
})
.then(response => {
console.log('response: ', response.data.content.token );
// If request is good...
// - Update state to indicate user is authenticated
dispatch({ type: AUTH_USER });
// decode token for info on the user
var decoded_token_data = jwt_decode(response.data.content.token);
// - Save the JWT token
localStorage.setItem('token', response.data.content.token);
console.log(localStorage.getItem('token'));
// - redirect to the appropriate route
browserHistory.push(ROOT_URL);
})
.catch(() => {
// If request is bad...
// - Show an error to the user
dispatch(authError('Bad Login Info'));
});
}
}

Resources