I am running this code:
.then((url) => {
if (url == null || undefined) {
return this.props.image;
} else {
const { image } = this.props;
//entryUpdate is an action creator in redux.
this.props.entryUpdate({ prop: 'image', value: url })
.then(() => {
this.setState({ loading: false });
});
but I get the following error:
How do I format setState() inside an asynchronous function that's called after an action creator?
Any help would be much appreciated!
In order for this to work, your action creator this.props.entryUpdate would need to return a promise for the async work it's doing. Looking at the error message, that does currently not appear to be the case.
You also need to be aware that calling setState() in the asynchronous callback can lead to errors when the component has already unmounted when the promise resolves.
Generally a better way is probably to use componentWillReceiveProps to wait for the new value to flow into the component and trigger setState then.
I placed the .then() function inside of the the if statement. But it should be like this:
.then((url) => {
if (url == null || undefined) {
return this.props.image;
} else {
const { image } = this.props;
this.props.entryUpdate({ prop: 'image', value: url })
}
})
.then(() => {
this.setState({ loading: false });
});
Related
I'm trying to use a callback function on a few refs I've created that will then dispatch an action to the redux store. The problem I'm running into is that when I try to use the callback I get an error saying that the function I'm trying to call within the callback is not a function. I assume this is because when I pass the child component into connect()() it looses it's original binding. Is there a solution to my problem?
//callback
sumbitQuiz = () => {
this.state.words.forEach((word) => {
return this[word].current.compairWordAndHeldWord()
})
}
//Function in child ref I'm trying to call
compairWordAndHeldWord = () => {
if( this.state.word === this.state.inputWord) {
this.setState(() => ({
wordAndInputWordMatch: true
}))
this.props.dispatch(addCorrectAnswer())
} else {
this.setState(() => ({
wordAndInputWordMatch: false
}))
}
}
I think that a way to do this i modify your addCorrectAnswer in order to return a promise from it, then (ehehe) you can handle the flow by:
compairWordAndHeldWord = () => {
if( this.state.word === this.state.inputWord) {
this.setState(() => ({
wordAndInputWordMatch: true
}))
this.props.dispatch(addCorrectAnswer())
.then(()=>{
//put here your callback logic
}
.catch(()=>{})
} else {
this.setState(() => ({
wordAndInputWordMatch: false
}))
}
}
I have promise in props: this.props.getProfile() and i want to set response value in promise into state before render()
i have tried with UNSAFE_componentWillMount and getDerivedStateFromProps but it always response a promise pending.
Here is my try with UNSAFE_componentWillMount:
UNSAFE_componentWillMount(){
this.setState({profile: this.getProfile()})
}
getProfile=()=>{
return this.props.getProfile()
.then(res =>{
if (res.type === 'ERROR_MESSAGE') {
return null;
}
return res.payload
});
}
Here is my try with getDerivedStateFromProps:
static getDerivedStateFromProps (props, state){
let a = props.getProfile()
.then(res =>{
if (res.type === 'ERROR_MESSAGE') {
return null;
}
return res.payload
});
console.log(a);
if(a.profile !== state.profile)
return {profile: a};
}
you might think that at a given promise once you return something inside then method you would get that value right away back, when actually you are returning other promise. to solve that you have 2 standard approaches, use async/await or chain another then to the promise.
getDerivedStateFromProps is not meant for data fetch as you can see a good discussion here
async\await
async componentDidMount() {
const profile = await this.props.getProfile()
this.setState({ profile })
}
promise chaining
componentDidMount() {
this.props
.getProfile()
.then(profile => this.setState({ profile }))
}
In my Context I have a LocalFunction that returns a promise.
LocalFunction: () => Promise<void>
LocalFunction: () => {
return externalCall.getBooks().then((books) => {
this.setState({ Books: books })
})
}
I can call this function in another component based on the updated Books object in the Context state like:
this.props.LocalFunction().then(() => {
// do something with this.props.Context.Books
})
But I know React updates states in batches. So could I run into a race condition when calling LocalFunction without the Books state being updated with the new books?
I know a way to avoid it is to wrap LocalFunction in a new Promise and resolve it in this.setState({ Books: books }, resolve), but I wanna avoid doing that if possible.
How about to use async/await?
LocalFunction: async (needUpdate = false) => {
const result = await externalCall.getBooks();
if(needUpdate){
this.setState({ Books: result })
}
return result;
}
this.props.LocalFunction().then((res) => {
console.log(res)
// do something with this.props.Context.Books
})
When you need to update state
LocalFunction(true)
I try to build a generic confirm component with redux and native promise. I read Dan Abramovs solution here: How can I display a modal dialog in Redux that performs asynchronous actions? but i am looking for a more generic appoach.
Basically i want to do this:
confirm({
type: 'warning',
title: 'Are you sure?',
description: 'Would you like to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort'
})
.then(() => {
// do something after promise is resolved
})
The confirm method basically opens the modal and returns a promise. Inside the promise i subscribe my redux store, listen for state changes and resolve or reject the promise:
export const confirm = function(settings) {
// first dispatch openConfirmModal with given props
store.dispatch(
openConfirmModal({
...settings
})
);
// return a promise that subscribes to redux store
// see: http://redux.js.org/docs/api/Store.html#subscribe
// on stateChanges check for resolved/rejected
// if resolved or rejected:
// - dispatch closeConfirmModal
// - resolve or reject the promise
// - unsubscribe to store
return new Promise((resolve, reject) => {
function handleStateChange() {
let newState = store.getState();
if (newState.confirmModal.resolved) {
store.dispatch(closeConfirmModal());
resolve();
unsubscribe();
}
if (newState.confirmModal.rejected) {
store.dispatch(closeConfirmModal());
reject();
unsubscribe();
}
}
let unsubscribe = store.subscribe(handleStateChange);
})
}
My confirm component is connected to redux store and is included once in some kind of layout component - so it is useable on all routes in the app:
class ConfirmModal extends Component {
constructor(props) {
super(props)
}
confirm() {
this.props.dispatch(resolveConfirmModal());
}
abort() {
this.props.dispatch(rejectConfirmModal());
}
render() {
// my modal window
}
}
export default connect(
state => ({
confirmModal: state.confirmModal
})
)(ConfirmModal);
Reducer/Action looks like this:
export const openConfirmModal = (settings) => {
return {
type: 'OPEN_CONFIRM_MODAL',
settings
};
};
export const resolveConfirmModal = () => {
return {
type: 'RESOLVE_CONFIRM_MODAL'
};
};
export const rejectConfirmModal = () => {
return {
type: 'REJECT_CONFIRM_MODAL'
};
};
export const closeConfirmModal = () => {
return {
type: 'CLOSE_CONFIRM_MODAL'
};
};
const initialState = {
open: false,
type: 'info',
title: 'Are you sure?',
description: 'Are you sure you want to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort',
};
export const ConfirmModalReducer = (state = initialState, action) => {
switch (action.type) {
case 'OPEN_CONFIRM_MODAL':
return { ...action.settings, open: true };
case 'RESOLVE_CONFIRM_MODAL':
return { ...state, resolved: true };
case 'REJECT_CONFIRM_MODAL':
return { ...state, rejected: true };
case 'CLOSE_CONFIRM_MODAL':
return initialState;
default:
return state;
}
};
The redux part is working. My confirm window can be open/closed and renders depending on my options. But how i can define a promise in my confirm method that can be resolved in my component? How i get everything connected?
Found a working Solution!
Found a solution that is pretty much what i was looking for:
The modal properties are driven by my Redux state
The modal component is included once AND it lives inside my
applicition not as a different rendered app like
here:http://blog.arkency.com/2015/04/beautiful-confirm-window-with-react/
The confirm method returns a native promise that is
resolved/rejected driven by Redux state
What do you think?
Well, you can do it, but it won't be pretty. Basically, you need a map of outstanding promises next to your confirm():
var outstandingModals = {}
const confirm = function(settings) {
return new Promise((resolve, reject) => {
let id = uuid.v4();
outstandingModals = resolve;
store.dispatch(
openConfirmModal({
...settings,
confirmationId: id,
})
);
}
and then later:
case 'CLOSE_CONFIRM_MODAL':
let resolve = outstandingModals[state.confirmationId];
if (resolve) {
resolve();
delete outstandingModals[state.confirmationId];
}
return initialState;
Like I said - ugly. I don't think you can do much better than that using promises.
But you can do better by NOT using promises. What I would do is simply render a Confirm component whenever necessary, say:
render() {
return <div>
... My stuff ...
{confirmationNecessary && <Confirm text='Are you sure?' onAction={this.thenConfirmed}/>}
</div>
}
confirmationNecessary can come from this.state or from the store.
I wrote a blog post that discusses one possible approach for managing "generic" modals like this: Posts on PacktPub: "Generic Redux Modals" and "Building Better Bundles".
The basic idea is that the code that requested the modal can include a pre-written action as a prop, and the modal can dispatch that action once it's closed.
There's also an interesting-looking library at redux-promising-modals that appears to implement modal result promises through middleware.
possible so :)
export const MsgBox = {
okCancel : s =>
new Promise((ok, cancel) =>
confirm(s) ? ok() : cancel() ),
......etc
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.