How to undo Redux Async Action? (multiple steps back in the state) - javascript

if I have an async action with api call, which could either be an action returns a function:
export function asyncAction(itemId) {
return (dispatch) => {
dispatch(requestStarted());
return sendRequest(itemId).then(
(result) => dispatch(requestSuccess()),
(error) => dispatch(requestFail())
);
};
}
or one returns an object and uses middleware to intercept it and do stuff:
export function asyncAction(itemId) {
return {
type: [ITEM_REQUEST, ITEM_REQUEST_SUCCESS, ITEM_REQUEST_FAILURE],
promise: sendRequest(itemId),
userId
};
}
// same middleware found in https://github.com/rackt/redux/issues/99
export default function promiseMiddleware() {
return (next) => (action) => {
const { promise, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest, readyState: 'request' );
return promise.then(
(result) => next({ ...rest, result, readyState: 'success' }),
(error) => next({ ...rest, error, readyState: 'failure' })
);
};
}
Now my question is: How do I rollback to the state before the asyncAction is dispatched, which essentially means two steps back in the state(success/failure => request) w/ an api call to undo last api call.
For example, after delete a todo item(which is an async action), a popup snackbar shows with an undo option, after click it the deleted todo item will be added back to UI along with an api call to add it back to db.
I've tried redux-undo but I feel it's not intended to solve problems like this.
Or I should forget about 'undo' and just dispatch a brand new addTodo action when user clicks undo option?
Thanks in advance :-)

Redux Optimist might be what you need.

Related

Proper way to handle a page refresh based on a redux request change

I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};

Pre-flight OPTIONS request delays my DELETE

I am deleteing an object then immediately retrieving a list of available objects, and am hitting a race-esk problem.
DELETE requests are subject to a CORS Pre-Flight OPTIONS request, while the GET request is not. This means my intended
DELETE /things/21
GET /things/
becomes:
OPTIONS /things/21
GET /things
DELETE /things/21
And the result of the GET includes object 21.
I want to avoid adding artificial delays; is there any other way to ensure the DELETE happens first?
(The requests are triggered from completely different components of my react app)
Edit:
I have a component Things which renders a summary list of things, and a component Thing which renders a page of detail.
Thing includes a Delete button, which fires a delete and navigates to Things. By "fires a delete" I mean: triggers an ajax DELETE to /things/21 and deletes thing 21 from my local redux store.
Things has a componentWillMount which triggers a GET to retrieve the list of available things, when they arrive my redux reducer adds them all to its store.
Edit: example:
Redux action creators
export const deleteThing = thingId => ({
type: 'DELETE_THING',
payload: thingId
});
export const retrieveThings = () => ({
type: 'FETCH_THINGS'
});
"Reducer" responsible for API requests
export default store => next => action => {
switch (action.type) {
case 'DELETE_THING':
return deleteObj(store.dispatch, action.type, `/things/{action.payload}`, action.payload);
case 'FETCH_THINGS':
return getObj(store.dispatch, action.type, '/things/');
}
}
const getObj = (dispatch, action, url) => {
return sendRequest(dispatch, () => fetch(url)
.then(processResponse(dispatch, action))
.catch(handleError(dispatch, action))
);
};
const deleteObj = (dispatch, action, url, payload) => {
return sendRequest(dispatch, () => fetch(url, {
method: 'DELETE',
headers
})
.then(results => {
if (results.status >= 400) return handleError(dispatch, action)(results);
// DELETE doesn't return anything but reducers want to know what was deleted, so pass the payload
return dispatch({
type: `${action}_SUCCESS`,
payload
});
})
.catch(handleError(dispatch, action))
);
}
// Wraps a fetch request, triggering redux events on send/receive (regardless of outcome)
const sendRequest = (dispatch, fn) => {
dispatch({type: 'SENDING_REQUEST'});
const always = () => dispatch({type: 'RECEIVED_RESPONSE'});
return fn().then(always, always);
}
Reducer/store for Things
export default (state = initialState, action) => {
case 'DELETE_THING_SUCCESS':
return state.deleteIn(['byId'], action.payload);
}
React Components
class Thing {
render () {
return (
<div>
<h1>{props.thing.id}</h1>
<button onClick={this.props.deleteThing}>Delete</button>
</div>
);
}
deleteThing () {
this.props.triggerActionToSend
// Pretend this is `connect`ed to a redux store
this.props.deleteThing(this.props.id);
// AJAX to delete from server
fetch({
url: '/thing/' + this.props.id,
method: 'delete'
});
// Redirect to main list
this.props.navigate('/things/');
}
}
// Pretend this is a `connect`ed component that receives a `things` prop from a redux store
class Things {
componentWillMount () {
this.props.retrieveThings();
}
render () {
return (
<ul>
{this.props.things.map(x => <li>x.id</li>)}
</ul>
);
}
);
const App = () => (
<div>
<Route path="/things" component={Things} />
<Route path="/thing/:thingId" component={Thing} />
</div>
);
Change your navigation call to wait for fetch promise to resolve. Should probably do same with deleting from local store but that isn't the issue at hand
// AJAX to delete from server
fetch({
url: '/thing/' + this.props.id,
method: 'delete'
}).then(_=> this.props.navigate('/things/'));

How to rewrite a basic action/reducer to use promises

I'm having trouble understanding how to rewrite normal action/reducer code to make use of redux-thunk or redux-promise-middleware so I can use promises.
I want to wait for my updatePhone to finish updating my state.user.information.phone before it starts testUserPhone. So obviously I need a promise to be returned from updatePhone.
this.props.updatePhone('+1**********')
.then(() => this.props.testUserPhone(this.props.user.information.phone))
action
export const updatePhone = (phone) => ({
type: UPDATE_PHONE,
payload: phone
})
and reducer
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case 'UPDATE_PHONE':
return {...state,
information: {
...state.information,
phone: action.payload
}
}
default:
return state
}
}
Should I write something like this as well as the basic function, or can I somehow combine them into one? Because I need the action to fully complete its cycle through my reducer and update phone before it comes back, but I don't want to break my reducer because now it won't be able to access payload and such since it's inside of a returned function -- super confused with how you start off using these libraries.
export function updatePhoneAsync(phone) {
return dispatch({
type: UPDATE_PHONE,
payload: phone
})
}
EDIT: So I've got this now for my action creators
export const updatePhone = (phone) => ({
type: UPDATE_PHONE,
payload: phone
})
export function updatePhoneAsync(phone) {
return function (dispatch) {
dispatch(updatePhone(phone))
}
}
Outside in my component;
this.props.updatePhoneAsync('+1**********')
.then(() => this.props.testUserPhone(this.props.user.information))
Which gives me an error 'cannot read property then of undefined'
You should write something like this if you use redux-thunk:
Action creators:
function update (params if you need them) {
return function (dispatch) {
send request here
.then(data =>
dispatch(phoneUpdated(data));
}
function phoneUpdated(phone) {
return {type: 'PHONE_UPDATED', phone};
}
Then, feel free to grab this action in your reducer and update the state as you wish.
Also, you can enhance it with additional actions in case when promise will be rejected, or at the start of request to show loader animations

Redux action not getting called the second time

I have two redux actions which call as follows.
export function action1(params) {
//This line is always called.
return (dispatch) => {
//This line is not called the second time.
return MyApi.call1(params)
.then(response => {
// some logic
return dispatch(someFunction1());
})
.catch(error => {
throw(error);
});
};
}
export function action2(params) {
return (dispatch) => {
return MyApi.call2(params)
.then(response => {
// call the first API again
action1();
return dispatch(someFunction2());
})
.catch(error => {
throw(error);
});
};
}
When the view is first loaded, action1 is called within the constructor of the view. Upon performing an action and triggering action2 in the same view, action1 needs to be called on action2's success to get the updated list from the server. Unfortunately, code breaks without any error when action1 is called the second time.
What am I missing here?
You have not dispatched the action1.
dispatch( action1( params ) )
Invoking action1() without dispatch just returns a function. In order to get dispatch in returned function, you should dispatch that function. Then it will be caught by redux-thunk middleware. The middleware will pass dispatch and invoke function.

return promise from store after redux thunk dispatch

I am trying to chain dispatches with redux thunk
function simple_action(){
return {type: "SIMPLE_ACTION"}
}
export function async_action(){
return function(dispatch, getState){
return dispatch(simple_action).then(()=>{...});
}
}
How do I get the dispatch to return a promise from the store?
MORE SPECIFICALLY:
I am probably just not understanding something here, but in all the examples with redux-thunk, they call a separate async event (like fetch), which obviously returns a Promise.
What I'm specifically looking for is when I dispatch an action to the store: How do I make certain the store has processed that action completely before anything else happens in the function action_creator() above.
Ideally, I would like the store to return some sort of promise, but I don't understand how or where that happens?
Here you have an example on how to dispatch and chain async action. https://github.com/gaearon/redux-thunk
The thunk middleware knows how to turn thunk async actions into actions, so you just have to have your simple_action() to be a thunk and the thunk middleware will do the job for you, if the middleware see a normal action, he will dispatch this action as normal action but if it's an async function it will turn your async action into normal action.
So your simple_action need to be a thunk ( A thunk is a function that returns a function.) Like this for example:
function makeASandwichWithSecretSauce(forPerson) {
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
When using the makeASandwichWithSecretSauce function you can use the dispatch function
store.dispatch(
makeASandwichWithSecretSauce('Me')
);
And even
// It even takes care to return the thunk’s return value
// from the dispatch, so I can chain Promises as long as I return them.
store.dispatch(
makeASandwichWithSecretSauce('My wife')
).then(() => {
console.log('Done!');
});
Here a complete example on how you can write action creators that dispatch actions and async actions from other action creators, and build your control flow with Promises.
function makeSandwichesForEverybody() {
return function (dispatch, getState) {
if (!getState().sandwiches.isShopOpen) {
// You don’t have to return Promises, but it’s a handy convention
// so the caller can always call .then() on async dispatch result.
return Promise.resolve();
}
//Do this action before starting the next one below
dispatch(simple_action());
// We can dispatch both plain object actions and other thunks,
// which lets us compose the asynchronous actions in a single flow.
return dispatch(
makeASandwichWithSecretSauce('My Grandma')
).then(() =>
Promise.all([
dispatch(makeASandwichWithSecretSauce('Me')),
dispatch(makeASandwichWithSecretSauce('My wife'))
])
).then(() =>
dispatch(makeASandwichWithSecretSauce('Our kids'))
).then(() =>
dispatch(getState().myMoney > 42 ?
withdrawMoney(42) :
apologize('Me', 'The Sandwich Shop')
)
);
};
}
//apologize and withdrawMoney are simple action like this for example
return {
type: "END_SUCESS"
}
//usage
store.dispatch(
makeSandwichesForEverybody()
).then(() =>
console.log("Done !");
);
To create you own promises you can use a library like bluebird.
//EDIT :
To be sure that the store has processed that action completely before anything else happens in the function action_creator() you can dispatch this simple_action before action_creator(); // I added this comment to the code //Do this action before starting the next one below
This is a pattern I've been using recently:
export const someThenableThunk = someData => (dispatch, getState) => Promise.resolve().then(() => {
const { someReducer } = getState();
return dispatch({
type: actionTypes.SOME_ACTION_TYPE,
someData,
});
});
When you dispatch(someThenableThunk('hello-world')), it returns a Promise object that you can chain further actions to.
dispatch will return whatever the action/function it calls returns; so if you want to chain certain activities (as per your example), your action would need to return a Promise.
As #Aaleks mentions, if your action were a thunk you can create a scenario where you return a Promise, then you could do as you mention.
BTW I think naming your thunk action_creator is a bit misleading, as simple_action is actually an Action Creator in Redux parlance - have edited accordingly :)
What you will need to do is create trunkate action which returns Promise. The dispatch function return what you have added as argument to it's call. For example, if you want dispatch to return Promise you'd have to add Promise as argument to the call.
function simple_action() {
return { type: 'SIMPLE_ACTION' };
}
export function async_action(dispatch, getState) {
return function () {
return Promise.resolve(dispatch(simple_action()));
}
}
const boundAction = async_action(dispatch, getState);
boundAction().then(() => {});
Asynchronous action and how to call an action from a component when using redux and thunk
Without Promise
action.js
export function shareForm(id) {
return function (dispatch) {
dispatch({
type: 'SHARE_FORM',
payload: source.shareForm(id)
})
}
}
SomeComponent.js
dispatch(shareForm(id))
With Promise
action.js
export function shareForm(id, dispatch) {
return new Promise((resolve, reject) => {
dispatch({
type: 'SHARE_FORM',
payload: source.shareForm(id)
})
.then(res => resolve(res))
.catch(err => reject(err))
})
}
SomeComponent.js
shareForm(id, dispatch)
.then(res => console.log('log on success', res))
.catch(err => console.log('log on failure', err))
PS: Let me know in comments if you need more explanations

Categories