mattkrick / redux-optimistic-ui

a reducer enhancer to enable type-agnostic optimistic updates
MIT License
693 stars 36 forks source link

Necessary to include request in middleware? #29

Open brandondurham opened 7 years ago

brandondurham commented 7 years ago

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?

dyst5422 commented 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 } },
    });
  }
Birkbjo commented 7 years ago

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).