Indexr introduces a higher-order selector which is dubbed a "table", because it acts a bit like a database table which is able to be "queried" by attribute.
A table is created via createTable
, which takes a selector
that is expected
to return a collection (can either be a map (e.g. if keyed by id) or a list),
and indices
, a mapping of ways to index that collection.
Here's an example:
import {createSelector} from 'reselect';
import {createTable} from '../indexr';
// normalized data in store
const getAllComments = state => state.comments;
// from UI store
const getSelectedPostId = state => state.selectedPostId;
// inefficient
const getCommentsForSelectedPost = createSelector(
getAllComments,
getSelectedPostId,
(allComments, selectedPostId) => {
return allComments.filter(comment => {
return comment.get('postId') === selectedPostId;
});
},
);
// more efficient
// build comments table "indexed" on postId
const commentsTable = createTable({
selector: getAllComments,
indices: {
byPostId: comment => comment.get('postId'),
},
});
const getCommentsForSelectedPost = createSelector(
commentsTable.indexedSelector('byPostId'),
getSelectedPostId,
(commentsByPostId, selectedPostId) => {
return commentsByPostId.get(selectedPostId);
},
);
In the inefficient example, whenever state.selectedPostId
changes, we do an
O(N) search, even if state.comments
doesn't change at all.
Using a table is more efficient because whenever state.comments
changes, the
table reconstructs the index, but every time state.selectedPostId
changes,
it's only an O(1) operation.
The indices
object accepts:
- function (called on the collection object, returns value to be used as index key)
- string (equivalent to a function that calls
.get(<string>)
on the object) - multibucket (see section below)
- array (use for multiple levels of indices - accepts functions, strings, or multibuckets as described before.)
Some more examples with output:
const byDayCreated = comment => DateTime.parse(comment.get('postId')).startOf('day');
const commentsTable = createTable({
selector: getAllComments,
indices: {
byDayCreated,
postId: "postId", // same as example above
byMultipleKeys: comment => ["postId", byDayCreated],
},
});
// multiple index usage:
commentsTable.indexedSelector("byMultipleKeys")(state)
=>
{
postId1: {
2019-02-20: <collection of comments for post1 written on 2019-02-20>,
2019-02-19: <collection of comments for post1 written on 2019-02-21>,
},
postId2: {
2019-02-20: <collection of comments for post2 written on 2019-02-20>,
}
}
Sometimes you want a single object to appear in multiple "groups"/"buckets" at once. This may occur if your object has an array key (even though your stores may be denormalized), or you can always provide your custom function that returns an array of possible indices.
import { createTable, multiBucket } from "../indexr";
const commentsTable = createTable({
selector: getAllComments,
indices: {
byTag: multiBucket("tags"),
// or
byTag: multiBucket(c => c.get("tags")),
},
});
// comments:
[{id: 1, tags: ["a"]}, {id: 2, tags: ["b"]}, {id: 3, tags: ["a", "b"]}]
commentsTable.indexedSelector("byTag")(state)
=>
{
"a": [{id: 1, tags: ["a"]}, {id: 3, tags: ["a", "b"]}],
"b": [{id: 2, tags: ["b"]}, {id: 3, tags: ["a", "b"]}],
}
Note multiBucket
s can be used in combination with other indices if desired:
const commentsTable = createTable({
selector: getAllComments,
indices: {
byPostIdAndTag: ['postId', multiBucket('tags')],
},
});
If you ever need the original, raw collection that was passed in as selector
to createTable
, you can access unindexedSelector
directly on the table.
createSelector(
commentsTable.unindexedSelector,
(comments) => {
...
}
)
If you need to do some transformation to the table's data ("reselect" the original table's source selector), you can use this function to generate a new table:
const getRatingFilter = state => state.ratingFilter;
const ratingFilteredCommentsTable = commentsTable.reselectSource(
getRatingFilter,
(comments, ratingFilter) => {
return comments.filter(c => c.get('rating') >= ratingFilter);
},
);
Note that the signature of reselectSource
is very similar to
createSelector
- except the very first argument (the original table source
collection) is implicit/omitted, and passed directly into the result function.
Note that a more performant solution in this case might be to add rating (or even the conditional expression) as an index, but sometimes the filter/transformation is more complicated. Regardless, this is not recommended for common usage.