Skip to content

Instantly share code, notes, and snippets.

@dra1n
Last active July 7, 2021 08:45
Show Gist options
  • Save dra1n/00dffcdab5598cf9572f3ab807bada89 to your computer and use it in GitHub Desktop.
Save dra1n/00dffcdab5598cf9572f3ab807bada89 to your computer and use it in GitHub Desktop.

Where we left

  • UI should be an afterthought
  • Write code that allows to sepearate business logic frow UI.
  • Write code that allows to inject any hard-to-test, too-verbose, want-this-mocked stuff logic.

Is redux a good tool for impelmenting these ideas?

  • It has special place for handling business logic, called middleware.
  • Popular middleware have a mechanism for context injection. If you are writing your own custom middleware, then you probably should think about adding this too.
/* redux-thunk */

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument(api)),
);
/* redux-saga */

const sagaMiddleware = createSagaMiddleware({
  context: {
    api
  }
});

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);
  • Reducers, actions and middleware give you a nice boundary for logic separation on three different levels.

A small cheat sheet

  • Have a service/services
// services/storageService.js

export default {
  getItem: global.localStorage.getItem,
  setItem: global.localStorage.setItem,
};
  • Have a place where you inject those services
export const configureStore = ({ storageService, analyticsService, graphqlMutationService }) => {
  const middleware = createSagaMiddleware({
    context: {
      storageService,
      analyticsService,
      graphqlMutationService
    }
  });
  
  const store = createStore(
    reducer,
    applyMiddleware(middleware)
  );
  
  middleware.run(rootSaga);

  return store;
};
  • Have everything mocked by default. If spec or storybook wants an a store instance, give them one. And it should be safe to play with. No accidental graphql calls, analytics calls, you get the point.
  • Make mocking code extensible and flexible, in other words allow providing different behavior for mocked services.
const analyticsServiceMock = {
  sendAgentFetch: () => {}
};

const graphqlMutationServiceMock = {
  submitAgentRequest: () => Promise.resolve(["success"])
};

export const configureStore = ({
  analyticsService = analyticsServiceMock,
  graphqlMutationService = graphqlMutationServiceMock
} = {}) => {
  const middleware = createSagaMiddleware({
    context: { analyticsService, graphqlMutationService }
  });
  
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(middleware)
  );

  middleware.run(rootSaga);

  return store;
};
  • Use that flexibility to cover all possible cases
it("displays an error on unsuccessful submit", () => {
  const graphqlMutationService = {
    submitAgentRequest: () => Promise.reject("something went wrong")
  };
  const store = configureStore({ graphqlMutationService });
  const component = mountRender(store);
  
  //...
  
  expect(errorMessage.exists()).toBeTrue();
})
it("updates error message when mutation was not successful", () => {
  const expectedError = new Error("something went wrong");

  graphqlMutation.mutate.mockImplementation(() =>
    Promise.reject(expectedError)
  );

  return expectSaga(mutationSaga, { callback, formValues })
    .provide([
      [getContext("graphqlMutation"), graphqlMutation],
      [select(submitDataSelector), submitData]
    ])
    .put(handleMutationFailure(expectedError))
    .run();
});

Bonus: redux mental model for those who likes OOP/OOD

  • Store is an object, actions are messages (methods)
  • Selectors are getters
  • Message handling is spread across reducers (setters) and middleware (business logic)
  • Message can have no handlers (ignored by reducers and middleware)
  • Existing message handlers can be extended by adding middleware or another "switch" condition in any of the reducers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment