Coder Social home page Coder Social logo

reselect-map's Introduction

reselect-map

npm

Selectors for mapping over collections.

Installation

npm install reselect-map reselect

Usage

This package exposes a few special selector creators. They're mostly the same as reselect's createSelector, with one major difference: the first dependency gets mapped over.

To give you a better idea, here's an example of how a selector to multiply an array of numbers would look in basic reselect and then using reselect-map.

import { createSelector } from "reselect";
import { createArraySelector } from "reselect-map";

const exampleState = {
  numbers: [1, 2, 3],
  multiplier: 5,
};

const numbers = (state) => state.numbers;
const multiplier = (state) => state.multiplier;

// reselect
const mul1 = createSelector([numbers, multiplier], (numbers, multiplier) =>
  numbers.map((n) => n * multiplier)
);

// reselect-map
const mul2 = createArraySelector(
  [numbers, multiplier],
  (number, multiplier) => number * multiplier
);

// Result: [5, 10, 15]

Notice the second version uses number instead of numbers; the result function is passed each element of numbers instead of the whole thing, and then the results are reassembled into an array.

So why would you use this? The answer is that you probably shouldn't! However, in certain situations it may significantly improve performance. Learn more in the motivation section.

API

The only thing to know is that the first dependency/selector passed is the one that gets mapped over, and all other selectors work just like they do in reselect proper. Additionally, key-based selectors like createObjectSelector and createMapSelector pass the key as the final argument.

NOTICE: This package makes use of the builtin Set and Map. If you need to support environments without Set or Map, you are going to have to polyfill it.

createArraySelector

Takes in an array, runs each element through the result function, and returns the results in a new array.

import { createArraySelector } from "reselect-map";

const exampleState = {
  numbers: [1, 2, 3],
  multiplier: 5,
};

const mul = createArraySelector(
  (state) => state.numbers,
  (state) => state.multiplier,
  (number, multiplier) => number * multiplier
);

console.log(mul(exampleState)); // [5, 10, 15]

createObjectSelector

Takes an object, runs the value at each key through the result function, and returns an object with the results. The key is passed as the last argument to the selector function.

import { createObjectSelector } from "reselect-map";

const exampleState = {
  numbers: { a: 1, b: 2 },
  multiplier: 5,
};

const mul = createObjectSelector(
  (state) => state.numbers,
  (state) => state.multiplier,
  (number, multiplier, key) => `${key}:${number * multiplier}`
);

console.log(mul(exampleState)); // { a: 'a:5', b: 'b:10' }

createListSelector

Takes anything with an array-like map function, and returns whatever that returns. This conveniently makes it compatible with Immutable js collections and similar without erasing the input type.

import { createListSelector } from "reselect-map";
import Immutable from "immutable";

const exampleState = {
  numbers: Immutable.List([1, 2, 3]),
  multiplier: 5,
};

const mul = createListSelector(
  (state) => state.numbers,
  (state) => state.multiplier,
  (number, multiplier) => number * multiplier
);

console.log(String(mul(exampleState))); // List [5, 10, 15]

createMapSelector

Like the sequence selector, but it expects the map function to provide a second argument to the callback that represents the key. This key is passed as the last argument to the selector function. This is mostly to support Immutable's Collection.Keyed types.

import { createMapSelector } from "reselect-map";
import Immutable from "immutable";

const exampleState = {
  numbers: Immutable.Map({ a: 1, b: 2 }),
  multiplier: 5,
};

const mul = createMapSelector(
  (state) => state.numbers,
  (state) => state.multiplier,
  (number, multiplier, key) => `${key}:${number * multiplier}`
);

console.log(String(mul(exampleState))); // Map { "a": "a:5", "b": "b:10" }

Need more?

I'm hoping that what I've got here covers most common use cases, but it's not difficult to expand. Please let me know if there are any other selector types you'd like, or submit a pull request!

Motivation

When doing very expensive computations on elements of a collection, reselect might not give you the granularity of caching that you need. Imagine a selector that looks like this:

import { createSelector } from "reselect";

const expensiveSelector = createSelector(
  (state) => state.largeArray,
  (largeArray) => largeArray.map(expensiveFunction)
);

Notice that every time largeArray is changed, every single element of the array will be run back through expensiveFunction. If largeArray is very large or expensiveFunction is very expensive, this could be very slow.

What would be better is if we only recomputed those elements that are new or have changed. That's what this package does. Your expensiveFunction only runs on the elements it needs to.

When shouldn't you use reselect-map

When you don't need it. If your selector takes an array and multiplies every element by five (like in all of the examples), this package will slow your code down. In situations where you're having performance issues with reselect and you're thinking about how to cache on an element level, this package is here for you.

reselect-map's People

Contributors

heyimalex avatar liborol avatar markwhitfeld avatar megawac avatar mjrussell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

reselect-map's Issues

Including the key in the mapper function

Great utility library! Ran into this exact scenario and was about to implement it myself until I stumbled across this.

One thing that would be great would be to add support for getting the key in the mapper function in createObjectSelector. For instance I might have an example like:

import { createObjectSelector } from 'reselect-map'

const exampleState = {
  numbers: { a: 1, b: 2 },
}

const mul = createObjectSelector(
  state => state.numbers,
  state => state.multiplier,
  (number, key, multiplier) => `${key}-${number * multiplier}`
)

console.log(mul(exampleState)) // { a: 'a-5', b: 'b-10' }

Lodash and other utility libraries typically provide the key also, typically as the second argument. Seems like it would be easy to change https://github.com/HeyImAlex/reselect-map/blob/master/src/memoize.js#L80 from

const result = fn(value, ...args)

to

const result = fn(value, key, ...args)

Obviously this is a breaking change and not every mapper would necessarily need the key, so thought I'd see what approach you'd take before submitting a PR

How does memoization work?

Hi there,

This looks very interesting for a use case that we have. But I have a question regarding memoization, because we're having an issue with the regular selectors from reselect.

Take this data set (it's a JSON API collection):

[
  { "type": "tasks", "id": "1", "attributes": { "name": "Do stuff" } },
  { "type": "tasks", "id": "2", "attributes": { "name": "Procrastinate" } }
]

We could create a selector with reselect, that would accept a function returning this array as the first argument and find a particular object in the callback:

const resourceSelector = createSelector(
  state => state.resourceCollection,
  resourceCollection => resourceCollection.find(res => res.id === '1')
);

The problem we have with this solution is that the memoization will not be optimized for our use case, as we would like for the selector to only run if the output actually changes.
Quick note: we build our own equalityCheck to check prev/next attributes for these particular objects.

I understand that this is due to the memoization looking at the input to the selectors, i.e. the collection of resources. So if another resource was added, it would not return the cached value and our component would re-render.

Would I be correct in assuming that this could be one of the use cases for reselect-map?

Our current workaround is simply to do this:

const giveMeResource = createSelector(
  resourceSelector,
  resource => resource
);

That's stupid ๐Ÿ˜ž

Customizing key used for memoization

The array/list selectors in this package have a problem: if you splice in some elements at index n, you lose all the memoization for everything that came after n.

Edit: now that I glanced at the code I see you have an optional undocumented equalityCheck argument. In a lot of cases it's probably more straightforward to just use a function that determines the cache key for a given element.

But often it's common to have an array of objects containing a property that can act as a key:

[
  {id: 1, name: 'Andy'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Martha'},
  ...
]

In this case if I could just provide a function that tells createArraySelector to use element.id as the key, then the results for each id would stay memoized no matter how the array was shuffled.

One way to do this would be to provide a keyBy option with the signature (value, originalKey) => newKey:

createArraySelector(
  state => state.users,
  state => state.multiplier,
  (user, multiplier) => user.age * multiplier,
  {keyBy: user => user.id}
)

Add typescript type definition file

I am using your library from a typescript project.

Please could you add a typing definition to the project so that it can be used in a type safe way.
Have a look at the 'reselect' module because they have done it there and your module is very similar.

Thanks!

Complex transformations

I currently have a reducer which looks something like the following:

const initialState = { 
    listing: [],
    byId: {},
    byContextId: {},
    filteredIds: [],
    filters: {},
    selectedContextId: null,
    selectedContext: null
}

export default function messages(state = initialState, action) {
    if (action.type == MESSAGES_RECEIVED_BATCH || action.type == MESSAGES_RECEIVED_ITEM) { 
        console.time(`[PERF] ${MESSAGES_RECEIVED_BATCH}`);

        let listing = state.listing;
        let byId = state.byId;
        let byContextId = state.byContextId;

        // sometimes I can get messages sent which I already have
        const newListing = [];
        action.messages.forEach(message => {
            if (!state.byId[message.id]) {
                newListing.push(message);
            }
        });

        if (newListing.length > 0) {
            // - index messages by id
            // - group messages by context id (with nested list and sub grouping by type)
            // - store messages in array

            let newById = {};
            let newByContextId = {};
            newListing.forEach(message => {
                const context = newByContextId[message.context.id] || { listing: [], byType: {} };
                message.types.forEach(type => {
                    context.byType[type] = context.byType[type] || [];
                    context.byType[type].push(message);
                });
                context.listing.push(message);

                newByContextId[message.context.id] = context;
                newById[message.id] = message;
            });
            for (var contextId in byContextId) {
                const context = byContextId[contextId];
                const newContext = newByContextId[contextId];
                if (newContext) {
                    newContext.listing.push(...context.listing);
                    Object.assign(newContext.byType, context.byType);
                }
                else {
                    newByContextId[contextId] = context;
                }
            }
            byContextId = newByContextId;
            byId = Object.assign(newById, state.byId);

            newListing.push(listing);
            listing = newListing;

        }

        ....

        console.timeEnd(`[PERF] ${MESSAGES_RECEIVED_BATCH}`);

        return {
            ...state,
            listing,
            byId,
            byContextId,
            filteredIds,
            selectedContext
        };
    }
    else if (action.type == MESSAGES_REQUESTED_ITEM) {
        return {
            ...state,
            selectedContextId: action.contextId,
            selectedContext: state.byContextId[action.contextId]
        }
    }
    return state;
}

To help paint a picture of what the above is doing, the following is the type of data I have and the structures that I'm trying to have "access" to (note, it's more complex than this but trying to simply things):

// input
action.messages = [
    { id: 1, context: { id: 1, ... }, types: [ 'a', 'b' ], payload: { ... } }, 
    { id: 2, context: { id: 2, ... }, types: [ 'c' ], payload: { ... } }, 
    { id: 3, context: { id: 1, ... }, types: [ 'a' ], payload: { ... } }, 
    { id: 4, context: { id: 1, ... }, types: [ 'c' ], payload: { ... } }, 
    { id: 5, context: { id: 1, ... }, types: [ 'd' ], payload: { ... } }, 
    { id: 6, context: { id: 2, ... }, types: [ 'c' ], payload: { ... } }, 
    { id: 7, context: { id: 2, ... }, types: [ 'a' ], payload: { ... } }, 
];

// output
state = {
    listing: [ ... ], // Simple: Unique list of messages by message.id
    byId: { ... },   // Simple: Indexed messages by message.id
    byContextId: {
        1: {
            listing: [ ... ], // Unique list of messages by message.id where context.id = 1
            byType: {
                'a': [ .... ], // Unique list of messages by message.id where message.context.id = 1 and message.type = 'a'
                'b': [ .... ], // Unique list of messages by message.id where message.context.id = 1 and message.type = 'b'
                ...
            },
            ... // others here
        }
    }, // Harder: Grouped by message.context.id
    ...
}

As you can see there are some complex but not extraordinary stuff going on here. There is also a lot which shouldn't be here and should be in selector.

Now, given the relative complexity of the transformation and number of messages that are being dealt with (range 500-5000), I'm keen not to have to recalculate everything each time I get a new message come in (which I'm currently defending against and selectors don't do out of the box). In thinking about shifting this logic into selectors, I'm trying to figure out how I'm best to only recalculate what has actually been affected... hence how I came across reselect-map.

When looking at the API I'm left wondering if there is currently support for my needs? What I'm thinking I need is a way to describe how the data should be "keyed"... In my case I would say that the key for byContextId is at message.context.id... then reselect-map would only make me recalculate the state for the contextIds that where received in received batch of messages and remember the state of the rest.

Any thoughts on how I should do this or I'm thinking about this incorrectly, etc.

Running into issues trying out the examples

require("reselect/package.json"); // reselect is a peer dependency. 
var reselectMap = require("reselect-map")
var createArraySelector = reselectMap.createArraySelector;

const exampleState = {
  numbers: [1, 2.5, 3],
  multiplier: 5
};

const mul = createArraySelector(
  state => state.numbers,
  state => state.multiplier,
  (number, multiplier) => number * multiplier
);

const result = mul(exampleState);
console.log(result)

https://runkit.com/embed/lzksevx8x4ok

I ran the above, and continuously get an empty array. I also tried to do the same with createObjectSelector, but it was hitting some crashes. Any help on this would be great.

Thanks!

Customizing the selector created for each element

Imagine each element of your array or collection is a deep object, say a big fat graphql result:

[
  {
    id: 1,
    name: 'Andy',
    orders: [
      {
        orderNumber: '23152',
        date: '2018-01-05',
        items: [
          {
            id: '182341',
            name: 'Fog Machine',
          },
          {
            id: '198234',
            name: 'Strobe Light',
          }
        ]
      }
    ]
  },
  ...
]

And you want to create an efficient selector for each element that picks out the user's name and the first item of the first order:

function createSelectorForElement(key) {
  return createSelector(
    user => user.name,
    createSelector(
      user => user.orders[0],
      (order = {items :[]}) => order.items[0],
      item => item & item.name
    ),
    (name, firstItem) => ({name, firstItem})
  )
}

Doesn't seem like there's currently a way to do this with reselect map? It would be nice to be able to pass a createSelectorForElement option.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.