modular-redux-thunk icon indicating copy to clipboard operation
modular-redux-thunk copied to clipboard

Suggestion: allow creating selectors and actions at the `combineModules` level

Open dallonf opened this issue 9 years ago • 0 comments

This idea probably needs a few revisions before it's workable in the library.

I've noticed that it is convenient to add actions and selectors in a combined module... for example:

// Sample submodules
const loading = {
  reducer: (state = false, action) => {
    switch (action.type) {
      case START_FETCH: return true;
      case FINISH_FETCH: return false;
      default: return state;
    }
  },
  selectors: { isLoading: (state) => state },
  actions: {},
}

const data = {
  reducer: (state = null, action) => {
    switch (action.type) {
      case FINISH_FETCH: return action.data;
      default: return state;
    }
  },
  selectors: { getData: (state) => state },
  actions: {},
};

const metadata = {
  reducer: (state = null, action) => {
    switch (action.type) {
      case FINISH_FETCH: return action.metadata;
      default: return state;
    }
  },
  selectors: { getMetadata: (state) => state },
  actions: {},
}

// Here comes the interesting stuff 

const moduleActions = {
  // These actions are used by multiple submodules within this
  // module, but not outside of it
  startFetch: () => ({ type: START_FETCH }),

  finishFetch: (result) => ({
    type: FINISH_FETCH,
    data: result.data,
    metadata: result.metadata
  }),

  // This action dispatches actions from within this module.
  // It may also use actions from submodules, although this isn't demonstrated here
  fetch: () => async (dispatch) => {
    dispatch(moduleActions.startFetch());
    const result = await fetchDataFromApi();
    dispatch(moduleActions.finishFetch(result));
    dispatch(moduleActions.)
  },
};

const moduleSelectors = {
  getEnhancedData = (state) => {
    // This line mentions "data" three times, not to mention having to name a
    // variable something similar!
    const d = data.selectors.getData(state.data);
    const m = metadata.selectors.getMetadata(state.metadata);
    return d.map(item => enhanceItemWithMetadata(item, m));
  },
};

const module = combineModules({
  loading, data, metadata,
});
// Adding these high-level actions and selectors is hacky, but workable
Object.assign(module.actions, moduleActions);
Object.assign(module.selectors, moduleSelectors);

Maybe something like this would be better:

// Actually pretty happy with the syntax here
const moduleActions = {
  startFetch: () => ({ type: START_FETCH }),

  finishFetch: (result) => ({
    type: FINISH_FETCH,
    data: result.data,
    metadata: result.metadata
  }),

  fetch: () => async (dispatch) => {
    dispatch(moduleActions.startFetch());
    const result = await fetchDataFromApi();
    dispatch(moduleActions.finishFetch(result));
    dispatch(moduleActions.)
  },
};

// Like global selectors, a list of selectors is passed as the first argument
const moduleSelectors = {
  getEnhancedData = (selectors, state) => {
    // ahhh much better
    const data = selectors.getData(state);
    const metadata = selectors.getMetadata(state);
    return d.map(item => enhanceItemWithMetadata(item, m));
  },
};

const module = combineModules({
  loading, data, metadata,
}), {
  // Ability to pass actions and selectors in an options field
  actions: moduleActions,
  selectors: moduleSelectors
};

This is pretty similar to the concept of global actions/selectors. Maybe I should have moved that logic to the level of combineModules in the first place.

dallonf avatar Sep 15 '16 15:09 dallonf