Reselect: Selectors with arguments?

Created on 26 Mar 2016  路  19Comments  路  Source: reduxjs/reselect

Is it possible with reselect to create selectors that take arguments? The selector would then return a normal selector function that accepts the state.

const someSelector = createSelector(
  [someOtherSelector, someOtherSelectorTwo],
  (argument, resultOne, resultTwo) => {}
);

const mapStateToProps = createStructuredSelector({users: someSelector('users')});

Or you know something like that? My use case is I have multiple parts of my state that share a common interface (they have a requirements object of the same signature). I want to create a selector that returns all results matching their requirements but currently I have to create one selector per entity (one for users that meet their requirements, etc).

Most helpful comment

@ttessarolo not good, since each call to getCategoryHistory will create new reselect selector, which will create new value even though, inputs didnt change. And that is exactly what you wanted to avoid by using reselect in the first place.

All 19 comments

Like this?

export const selectorEntityList = state => state.entities.get(schema.dataset.getKey()) || new Map();

export const selectorEntity = id => {
  return createSelector(
    selectorEntityList,
    (list) => list.get(id) || new Map()
  );
};

export const selectorAAA = createSelector(
    selectorEntity(10),
    (entity) => { console.log(entity); }
  );
};

but i have similar question....how to create selector with deep nesting? I just can't solve the puzzle :(

how to select the 'attribute'?:

listA [10].listB[5].listC[6].attribute

10, 5, 6 - another parts of global state.
i tried to get it like this:

export const selectorDatasets = state => state.entities.get(schema.dataset.getKey()) || new Map();
export const selectorDataset = dataset_id => {
  return createSelector(
    selectorDatasets,
    (datasets) => datasets.get(dataset_id.toString()) || new Map()
  );
};

export const selectorDashboards = dataset_id => {
  return createSelector(
    selectorDataset(dataset_id),
    (dataset) => dataset.get(schema.dashboard.getKey()) || new Map()
  );
};

export const selectorDashboard = (dataset_id, dashboard_id) => {
  return createSelector(
    selectorDashboards(dataset_id),
    (dashboards) => dashboards.get(dashboard_id) || new Map()
  );
};

this one is never refresh 'dashboard':

const selectorDatasetId = state => state.fakeRoute.dataset_id;
const selectorDashboardId = state => state.fakeRoute.dashboard_id;
export default createSelector(
  selectorDatasetId,
  selectorDashboardId,
  state => state,
  (dataset_id, dashboard_id, state) => {
    const dashboard = selectorDashboard(dataset_id, dashboard_id)(state);
    console.log(dashboard);

    return {
      dataset_id,
      dashboard_id,
      dashboard
    }
  }
);

but 'dashboard' never refresh :(

Yeah it just dawned on me I can just create a function that returns a createSelector call, I'm an idiot.

can you rename topic for getting deep nesting state via arguments? :))))) i don't want to create another issue, but not sure that people will look here - with current title :(

@Anahkiasen You definitely aren't an idiot, it's a pretty common question.

@plandem, thanks a lot for taking the time to the answer the original question!

As regards to your follow-up question, I can't quite follow the example but it looks like you are returning a selector as the output from selectorDashboards and then passing that into the selector created in selectorDashboard which doesn't seem right.

Also, if you create new selectors every time the state changes it will prevent Reselect from being able to memoize the results.

There are two sections in the documentation starting here that might be along the lines of what you are trying to achieve. If I've missed the point of what you are doing I'll be happy to help out more if you can give me an example that I can run on my machine (or a jsfiddle/jsbin).

By the way, rather than changing the title of this issue I actually think it is better to open a second issue for your follow-up question. That way people in the future that looking through the issues will stand a better chance of being able to find the answers to both questions.

here is issue #101

Cool :+1:

@ellbee so is it not correct way to use?

const selectorDatasetId = state => state.fakeRoute.dataset_id;
const selectorDashboardId = state => state.fakeRoute.dashboard_id;
const selectorDashboard = state => storeSelectors.selectorDashboard(selectorDatasetId(state), selectorDashboardId(state))(state);

export default createSelector(
  selectorDatasetId,
  selectorDashboardId,
  selectorDashboard,
  (dataset_id, dashboard_id, dashboard) => {
    return {
      dataset_id,
      dashboard_id,
      dashboard
    }
  }
);

It depends on the context, but I will say that it looks like it might be a mistake. Can you show me how you are using this in the context of a React Redux app?

well, i have deeply nested immutable state tree(consider it like snapshot of real DB), like this:
https://www.dropbox.com/s/j8qkpft7la782m2/Screenshot%202016-03-27%2000.34.00.png?dl=0

at the same page, some components use entities from 'dashboards', other components use entities from 'dashboard_views'. [1] and [2] at the path of above screenshot are state of router.

I'm trying to isolate each component with only required part of state, i.e. component that render 'dashboard_view' knows nothing about 'dashboard' and has no any property with it.

so to get dashboard_view i must get right dataset entity via dataset_id, after that i must get right dashboard via dashboard_id at relation dashboards of this dataset and finally get right dashboard_view at relation of dashboard_views of this dashboard.

I.e.:

entities -> datasets -> 2 -> dashboards -> 1 -> dashboard_views -> 94

Specifically, the bit I think may be a mistake is that it looks like the storeSelectors.selectorDashboard is returning a selector. And then that selector is being passed into the result function. So the object the selector returns has a type like:

 {
      dataset_id :: number,
      dashboard_id :: number,
      dashboard :: function
 }

But without seeing a complete example it is hard to tell that is what it is supposed to be doing.

Right now i'm trying to use it like this:

const selectorDashboard = state => storeSelectors.selectorDashboard(selectorDatasetId(state), selectorDashboardId(state))(state);

this one return part of state. But with such deep nested tree of state, looks like it does not cache :(

Ah right, I missed the final state on the end, so ignore what I said about that part.

As for it not caching, If you create new selectors every time the state changes it will prevent Reselect from being able to memoize the results. A new selector has an empty cache so it has to perform the computation.

any recommendation for such kind of deeply nested state tree? i have feeling that this code smells :)

p.s.: i don't want to create a new selector each time, but datasets, dashboard and dashboard_views fetching via api, so some parts of state changes alot (like loading/success and etc), but datasets part of tree changes not so often.

@plandem : certainly the common recommendation for nested/relational Redux data is to flatten it as much as possible. See the "nested state" question in the FAQ.

You might also be interested in a higher-level abstraction like https://github.com/tommikaikkonen/redux-orm , which is specifically intended to deal with this kind of data. It lets you define "Models" and their relationships, then treats a portion of your store as if it were a database with "tables" for each Model type. Looking up a given Model instance by ID automatically retrieves other related items for you.

any recommendation for such kind of deeply nested state tree?

@plandem A use case for this is when you maintain detailed data for a list of entities, where the detailed data for each entity is fetched separately as they are needed.

Example

Imagine you have a view that displays a list of many products. In another view you show the details for a single product. You only want to fetch the detailed data for a product if you are going to display it.

The simple way to do this is to only store the detailed data for one product at a time, but that will require you to fetch the detailed data every time you switch between the details of two products.

By having a reducer that holds details for all products (for which you have fetched the details), you don't have to throw away details for the previous product(s) when fetching details for new products.

In the latter case, a memoized selector would recompute whenever we want to show details for a new product, which might be fine. However, what if you need to show details for several products at a time?
If several components use the memoized selector to get details for different products, the selector will keep throwing away the previous results and would have to recompute the details for every product every time.

In that case it would be beneficial to memoize the derived details _per product id_, so that we can reuse the derived details for a product unless the data in the reducer has been updated.

One way to achieve this is to create your own createSelector function with a custom memoization function. An example is shown below. (I'm not sure it's necessarily the best way to do it.)

import { createSelectorCreator } from 'reselect';

const createSelectorForProductId = createSelectorCreator((resultFunc) => {
    const memoAll = {};

    return (productId, ...args) => {
        if (!memoAll[productId]) {
            memoAll[productId] = {};
        }

        const memo = memoAll[productId];

        if (!shallowEquals(memo.lastArgs, args)) {
            memo.lastArgs = args;
            memo.lastResult = resultFunc(...args);
        }

        return memo.lastResult;
    };
});

const _getProductIdAsMemoKey = (state, productId) => productId;
const getRawDetailsForProduct = (state, productId) => state.productDetails[productId];

export const getDetailsForProductId = createSelectorForProductId(
    _getProductIdAsMemoKey,
    getRawDetailsForProduct,
    (rawDetails) => {
        // derive data from raw details data
        ...
    }
);

// Use selector
getDetailsForProductId(state, productId);

The memoization function passed to createSelectorCreator assumes that the first dependency resolves the productId for the product you want details for, while _getProductIdAsMemoKey and getRawDetailsForProduct and thus getForProductId requires that the productId is passed as 2nd parameter.

Is so simple: let's give you an example, I have a mapStateToProps like this:

function mapStateToProps(state) {
  return {
    categoryHistory: getCategoryHistory(state,'extended')
  }
}

then I've create a selector like this:

export const getCategoryHistory = (state, type) => createSelector([getTaxonomy, selectedCategoryID], (categories, categoryID) => categories.getIn([type, categoryID]) || [])(state)

The solution is to call createSelector() passing the state as parameters:

createSelector()(state)

inside the selector you can use all the parameter you want to pass through.

@ttessarolo not good, since each call to getCategoryHistory will create new reselect selector, which will create new value even though, inputs didnt change. And that is exactly what you wanted to avoid by using reselect in the first place.

I think you can close this issue as its explained in FAQ's
https://github.com/reduxjs/reselect#q-how-do-i-create-a-selector-that-takes-an-argument

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jwoudenberg picture jwoudenberg  路  3Comments

maxim1006 picture maxim1006  路  3Comments

watch-janick picture watch-janick  路  5Comments

sarink picture sarink  路  4Comments

MrDesjardins picture MrDesjardins  路  3Comments