feathers-redux icon indicating copy to clipboard operation
feathers-redux copied to clipboard

Differentiate results

Open 2manyvcos opened this issue 7 years ago • 8 comments

I like the approach of this package and the simplicity on how it works.

Edit I tested a lot and simplified this issue based on my tests:

Please look at following situations to understand my issue:

  • Situation A: I need to find a list of data sets based on different query options. If I wanted to display two lists of data sets on the same page I would need to have two reducers right now in order to distinguish the results and to prevent them from being overwritten.
  • Situation B: I get a data set from the server. I have an error message field which should print a warning if the data could not be fetched. When the data was successfully fetched from the server I want to change it somehow. So I do an update. If the update failed and I now go back to my preview of the data I get an error message which I can't tell if it was from the get or the update, so I have to add additional state to distinguish.
  • Situation C: I find an array of data sets from my server which give me basic information about my entries. I now want to get each entry of the same endpoint afterwards and load them into my view so that my application doesn't hang and the user wouldn't need to wait until all of the data was fetched before he could interact with the UI. How should I get both the result of the find and all results of get into the state?

So here are some possible solutions:

  • Solution for situation A: You could set something like a 'namespace' in the action to let the reducer know in which substate the result should be put. This would give the user most control about the data but would cause a lot of substates which may never be reused afterwards. I also don't think that this would be redux conform as you would modify the way the reducer works in the actions which should be prevented if possible.
services.myService.find/get/...('namespaceA', data);
services.myService.find/get/...('namespaceB', ...);
myService: {
  namespaceA: {
    isError, isLoading, isFinished, data, ...
  },
  namespaceB: {
    isError, ...
  }
}
  • Solution for situation B: You could add a field like 'method' to the state.
services.myService.get(...);
myService: {
  method: 'get',
  isError,
  data: { entryData },
  ...
},
  • Solution for situation C: A mechanism to inject the result into queryResult?

Please let me know what you think about this.

2manyvcos avatar Aug 09 '17 16:08 2manyvcos

Something like the following could be implemented as well to make the state more flexible:

options:

{
  ...,
  shape: ..., // <-- the mapping - if undefined, the current reducer should be returned directly
}
methodReducer.js
import { combineReducers } from 'redux';
import _ from 'lodash';

const childReducer = (reducer, keywords, initState = {}) => (state = initState, action = {}) =>
  (_.isString(action.type) && keywords.find((keyword) => action.type.indexOf(keyword) !== -1))
  ? reducer(state, action)
  : state;

const stringSafe = (value) => _.isString(value) ? [value] : value;

const normalizeMappingValue = (value) => {
  const sValue = stringSafe(value);
  if (_.isArray(sValue)) return normalizeMappingValue({ keywords: sValue });
  if (_.isObject(sValue)) {
    const { keywords, ...rest } = sValue;
    const sKeywords = stringSafe(keywords);
    return {
      ...rest,
      keywords: sKeywords.map((keyword) => `_${keyword.toUpperCase()}`),
    };
  }
  return { keywords: [] };
};

const normalizeMapping = (mapping) => {
  const sMapping = stringSafe(mapping);
  if (_.isArray(sMapping)) {
    return normalizeMapping(_.reduce(
      sMapping,
      (sum, val) => ({ ...sum, [val]: val }),
      {}
    ));
  }
  if (_.isObject(sMapping)) {
    return _.reduce(
      sMapping,
      (sum, value, key) => ({
        ...sum,
        [key]: normalizeMappingValue(value),
      }),
      {}
    );
  }
  return {};
};

export default (reducer, mapping) => !mapping ? reducer : combineReducers(
  _.reduce(
    normalizeMapping(mapping),
    (sum, { keywords, initState }, key) => ({
      ...sum,
      [key]: childReducer(reducer, keywords, initState),
    }),
    {}
  )
);
Usage
import methodReducer from '...';

const likeNowReducer = methodReducer(services.myService.reducer); // same as return services.myService.reducer

const verySimpleReducer = methodReducer(services.myService.reducer, 'find'); // maps only find
/*
{
  find: { ...results of find }
}
*/

const simpleReducer = methodReducer(services.myService.reducer, ['find', 'get', 'create', 'update', 'patch', 'delete']); // maps all methods into different substates
/*
{
  find: { ...results of find },
  get: { ...results of get },
  create: { ...results of create },
  update: { ...results of update },
  patch: { ...results of patch },
  delete: { ...results of delete }
}
*/

const advancedReducer = methodReducer(services.myService.reducer, {
  find: 'find',
  get: 'get',
  put: 'create',
  alter: [ 'update', 'patch' ],
  delete: 'delete'
}); // maps find, get and delete normally but maps create to put and combines update and patch to one substate named alter
/*
{
  find: { ...results of find },
  get: { ...results of get },
  put: { ...results of create },
  alter: { ...results of update and patch },
  delete: { ...results of delete }
}
*/

2manyvcos avatar Aug 09 '17 20:08 2manyvcos

Just want to let you know I'm still thinking about it.

eddyystop avatar Aug 12 '17 13:08 eddyystop

Related is https://github.com/feathersjs/feathers-redux/issues/35 Related https://github.com/feathersjs/feathers-redux/issues/24

eddyystop avatar Sep 18 '17 12:09 eddyystop

I ran into this issue while building a basic redux + feathers application. For now I'm using the base feathers client combined with redux and redux-saga.

plhosk avatar Oct 04 '17 03:10 plhosk

Any update on this? The base functionality is great, but Situation A is really common.

Edit - Ended up using the following, seem to provide the functionality I was after.

export function feathersReduxServices(app) {
  return {
    ...reduxifyServices(app, [
      'users',
      'articles',
    ]),
    ...reduxifyServices(app, { articles: 'topArticles' }),
  };
}

Sicria avatar Nov 07 '17 08:11 Sicria

I'm tied up with a lot of feathers development, and documentation for the new version right now.

Changes to feathers-redux are on the road-map, including an optional immutable store, but I won't realistically get to it before Jan 2018.

eddyystop avatar Nov 07 '17 13:11 eddyystop

@Sicria Using this and the state is not being modified correctly... Are you sure this is working?

I'm using the same set-up that you are, but when I dispatch a find on 'articles', both 'articles' and 'topArticles' are modified.

Dispatching find on 'topArticles' causes neither of them to be modified, but I'm able to access the result from the promise.

Has anyone found a solution?

Edit: Never mind: I had my reducers set up incorrectly. Thanks for the solution!

greyivy avatar May 28 '18 17:05 greyivy

@hauckwill Glad you got it working, it's not the most elegant solution but it works for now.

Sicria avatar May 29 '18 01:05 Sicria