Open brandondurham opened 7 years ago
No!
Ok, so I fumbled around with this for a while. Here is what came up with in case anyone else happens upon this. I completely skip the middleware. After all, Thunks, Sagas etc are just processed in middleware anyways (so REALLY I just happen to be Thunk as my middleware piece and get clever in my action)
I happen to be using Thunk's to do this, but any asyncronous dispatching should be fine
let nextTransactionID = 0;
async function thunkAction(): ThunkAction<Promise<void>, IReduxState, undefined> {
const transactionID = nextTransactionID++;
try {
// Optimistic dispatch
dispatch({ ...newDataActionCreator(optimisticData),
meta: { optimistic: { type: BEGIN, id: transactionID } },
});
const response = await fetch(...);
const realData = await response.json().data;
// Ok, now lets rewrite with our real data
// note that the state after optimistic dispatch is the starting point for this dispatch
// Either have a different action here, or ensure that applying the action with the real data AFTER
// the action with the optimistic data gives you your desired state
dispatch({ ...newDataActionCreator(realData),
meta: { optimistic: { type: COMMIT, id: transactionID } },
});
} catch(err) {
// Here, the state is reverted to before the optimistic dispatch before applying the next dispatch
dispatch({ ...errorActionCreator(new Error('Fetch failed')),
meta: { optimistic: { type: REVERT, id: transactionID } },
});
}
I know this is an old issue, but I will show another example for good measure. This is currently how I do it in my app, using redux-observables. This combination has been wonderful to use, so thanks a lot for this library! The great thing about this approach is that the middleware handles the IDs, while the epics only really decide if it should be committed or reverted.
//ReduxOptimisticMiddleware.js
import { BEGIN, COMMIT, REVERT } from "redux-optimistic-ui";
//All redux action types that are optimistic have the following suffixes
const _SUCCESS = "_SUCCESS";
const _ERROR = "_ERROR";
//Each optimistic item will need a transaction Id to internally match the BEGIN to the COMMIT/REVERT
let nextTransactionID = 0;
export default store => next => action => {
// FSA compliant
const { type, meta, error, payload } = action;
// Ignore actions without isOptimistic flag
if (!meta || !meta.isOptimistic) return next(action);
const isSuccessAction = type.endsWith(_SUCCESS);
const isErrorAction = type.endsWith(_ERROR);
//Response from server, handled in epic-middleware
if (isSuccessAction || isErrorAction) {
return next(action);
}
// Now that we know we're optimistically updating the item, give it an ID
let transactionID = nextTransactionID++;
// Sending to server; extend the action.meta to let it know we're beginning an optimistic update
return next(
Object.assign({}, action, {
meta: { optimistic: { type: BEGIN, id: transactionID } }
})
);
};
//epics.js
const editApp = action$ =>
action$.ofType(actions.APP_EDIT).concatMap(action => {
const { app, data } = action.payload;
return api
.updateApp(app.id, data)
.then(resp =>
actionCreators.commitOrRevertOptimisticAction(
actionCreators.editAppSuccess(app, data),
action
)
)
.catch(error =>
actionCreators.commitOrRevertOptimisticAction(
actionCreators.actionErrorCreator(
actions.APP_EDIT_ERROR,
error
),
action
)
);
});
export default combineEpics(editApp);
//actionCreators.js
const optimisticActionCreator = action => ({
...action,
meta: { ...action.meta, isOptimistic: true }
});
export const commitOrRevertOptimisticAction = (
action,
transaction,
error = false
) => {
if (action.error) {
error = true;
}
let transactionID = transaction;
if(transaction && transaction.meta && transaction.meta.optimistic) {
transactionID = transaction.meta.optimistic.id;
}
return {
...action,
meta: {
...action.meta,
optimistic: error
? { type: REVERT, id: transactionID }
: { type: COMMIT, id: transactionID }
}
};
};
//This is what my actionCreators will look like
export const optimisticEditApp = (app, data) =>
optimisticActionCreator(
actionCreator(actions.APP_EDIT)({
app,
data
})
);
That's it! All I do to have an action being optimistic is decorating it with optimisticActionCreator. In my views I can just do dispatch(optimisticEditApp)
.
I have all of my requests set up to fire within my Sagas. I noticed in your example you use
socket.emit
and then the response to create a Redux action.Is it necessary to have the
socket.emit
request in the middleware for redux-optimistic-ui to work? If it isn’t necessary, are there any examples that illustrate that?