Reselect v5 Roadmap Discussion: Goals and API Design #491
Replies: 15 comments 32 replies
-
Thanks for putting this together @markerikson! Really hope this gains some traction. |
Beta Was this translation helpful? Give feedback.
-
So should we focus on Redux + Reselect, or only on Reselect? Some API decisions can be different, as Redux can hide some extra operations behind its own facade, including:
For the cases without Redux Reselect can look towards atomic state managers like: jotai or reatom, as they all are about Tecnhically speaking - two cases can be combined into one, and reselect can feed redux "model", not actually doing anything extra, letting redux manage relationship between used atoms as long as it "can" do it. |
Beta Was this translation helpful? Give feedback.
-
It's tricky to contribute to this because you've so comprehensively covered all of the issues at play here, and so it feels like there's little to actually say other than adding weight to the specific problems I face with Reselect. It may be that one of reasons you've perhaps not gotten the engagement you wanted on this is because you've already laid out a lot of the thoughts people would want to contribute, and extremely comprehensively. All of that being said, let me try and add some weight. (Please don't expect any of this to be unique or anything different to what you've said before) I agree that one of the major problems with Reselect is the intersection between selectors that depend on non-state inputs, and a cache size being fixed at 1 – especially in connection to React where you may be frequently running the selector with different parameters (both in terms of store state and arguments). I understand that there are arguments that if you didn't expose the create selector factory, there are more options for fixing the above problem. In my experience, using the create selector factory has always felt like a hack to work around a different problem. And if you solve this problem, you may be able to reduce use of the create selector factory to an even lower level than currently. Wherever I have used the create selector factory, it has been to work around issues where an input selector has a non stable output (i.e, running it twice with the same input gives you referentially different outputs, but this input selector has its own inputs that are wider than strictly necessary) and I don't want this selector to run on every state change, because its directly depended on by In my usage of Reselect, component renders have always been a more expensive process than selector computation – I've rarely particularly cared about how frequently selectors run. I do care that by the time they're used in To be clear, my main motivation for using reselect is reducing React re-renders where my selectors are doing cheap derivation that results in referentially unequal results call-to-call, not to avoid running expensive state computations. I don't know if that puts me in a different place than most users of re-select. Consider the following structure – a list of files that each have contents in them, and then a selector that returns a list of file names. const selectFiles = state => state.files;
const selectFileNames = createSelector([selectFiles], files => files.map(file => file.name)); If I use I.e, something like this const createCompareEqualForKeySelector = (
keySelector: (_: unknown, __: unknown, key: any) => boolean | undefined
) => (currentEntities: any, previousEntities: any) => {
if (currentEntities.length !== previousEntities.length) {
return false;
}
for (let idx = 0; idx < currentEntities.length; idx++) {
const currentEntity = currentEntities[idx];
const previousEntity = previousEntities[idx];
if (!isEqualWith(currentEntity, previousEntity, keySelector)) {
return false;
}
}
return true;
};
const createFieldSelector = <T, F extends keyof T>(
selector: (state: any) => T[],
field: F
) =>
createSelectorCreator(
defaultMemoize,
createCompareEqualForKeySelector((curr, prev, key) =>
!key || field === key ? undefined : true
)
const selectFiles = state => state.files;
const selectFileNames = createFieldSelector(
selectFiles,
"name"
); I think if you make it easier for selectors to return a previous output if they previous output and the new output are equal by some user-defined predicate, then you can reduce the need for In terms of approach, I think you may be biting off more than you can chew with trying to solve (or at least consider/address) all of the topics in this discussion at once. I think you should try and address as many things as you can in the above that can be done without a breaking change, and then you'll be able to identify what really are the priorities for the next breaking change. Basically I'm saying there's a fair amount that can be done to improve reselect (not that it isn't already brilliant) without a breaking change – you're not there yet, and I think waiting for a breaking change might be holding some real QoL changes back. Another thing to mention is that I think the lack of a sanctioned "Reselect dev tools" makes it really hard to know if you're using reselect well or not. I'd love something that showed me my reselect "tree", and also track how frequently memoization is a cost vs a benefit to performance (i.e, how frequently did the memoization check save you from running this selector). This could help identify where reselect is costing you more than its benefitting you. I'd also contend that the main benefit of Reselect is not memoization (I really don't find myself doing expensive state derivation in selectors very often, its the composition API – especially in TypeScript). I'd love a way of using the composition API whilst opting out of memoization – and maybe then the dev tools can use some heuristic for suggestion when a selector should be memoized. I think the fact that we have entirely different syntax for memoized/non memoized selectors results in an overuse of reselect, and if it was more akin to flipping a flag or changing the name of the create function used, we'd get smarter about when to use/not use memoization. You see this in the React world where the team are frequently arguing for not using memoization as a default – this is a hard argument to make in the world of Reselect when you lose the composition API if you want to turn of memoization. Finally, I definitely think a breaking change should remove variadic arguments. I know it's easier to type now, but I still find them harder to read, and it also prevents you easily adding a "configuration" object to the Just some musings on my thoughts – I don't know how valuable they are. Sorry it's taken so long to get these out! |
Beta Was this translation helpful? Give feedback.
-
Hi there! @markerikson makes huge research, as always, I found out something new from this post, thanks 👍 But I want to point one more problem about the reselect and a selector. It encourages an inconsistent state if an error was thrown from it. Check this example. One of the solutions is allowed to call a selector inside a reducer. Here some related project - topologically-combine-reducers, reatom. Also, I think limitations about standard Map/Set in |
Beta Was this translation helpful? Give feedback.
-
Here are some thoughts I started on a while ago but have updated since seeing some of the other replies. There have been some fantastic discussion points so far. Mark's tweet lit a fire under my butt to finish off this post. Update on type changesI'm currently (very slowly) working on finishing off the non-breaking changes for v4 (see #486). I was porting over the changes to v5 simultaneously, but I now think we should hold on to those until this discussion is brought closer to resolution. Rewiting in TypeScriptI'm, personally, all for this idea. I think it's a reasonable assumption that TypeScript will pretty much take over as the lingua franca for web development. However, this suggestion, in and of itself, isn't that "important". Well, at least there isn't much to discuss about it. It's a fairly binary opinion; you either agree or disagree. I think a simple poll with favorable results towards moving to TS is enough to justify moving forward.
Yeah, this is definitely true. I think rewriting the whole thing in TS will help simplify this by quite a bit. You also get the added benefit of the implementations exercising the type definitions automatically. What should be brought into v5? (sans types)@markerikson listed these issues out in exquisite detail, so I won't repeat them here. I'll sprinkle in my opinions here and there. Cache Size and Selector Instance ReuseThis is a fascinating problem. There are many good suggestions to solve this already. I personally think
It might be worth discussing either incorporating or absorbing this functionality into Optimizing Comparison BehaviorI think this could be solved with a solution inspired by
Some default "result memoizers" could be provided out of the box. Something like export const shallowArrayMemoizer = <T>(prev: T[], next: T[]): T[] => {
const sameLength = prev.length === next.length
const sameValues = sameLength && prev.every((val, idx) => val === next[idx]);
return sameValues ? prev : next;
} This would even allow for composing "cached selectors" with "cached result selectors". If this approach is to be entertained, it might be worth considering renaming "cached selectors" to "cached input selectors" to differentiate them from the "cached result selectors". I dunno, just spitballing here. There might be massive holes with this approach that I don't see. Debugging Selector RecalculationsThis is an awesome problem, but I haven't thought about it enough to even comment, honestly. I'll think about it and come back later if I have anything I think might help the conversation. TLDR
|
Beta Was this translation helpful? Give feedback.
-
I've read this conversation once again, and look like there are 3 different use cases we are trying to solve:
And then let's look at this separation from WeakMap based cache control (like kashe), where "single cache entry" is replaced by WeakMap
Let's try to solve a few examples: const selectTodoDescriptions = createSelector(
selectTodos,
todos => todos.map(todo => todo.text)
)
const selectItemsByCategory = createSelector(
state => state.items,
(state, category) => category,
(items, category) => items.filter(item.category === category)
) Is a more complicated case, as const selectItemsByCategory = createSelector(
state => state.items,
(state, category) => getPicker(/*from*/items, /*what*/category),
(items, category) => items.filter(item.category === category.value)
)
// where
getPicker = (source: any, value:any) => {
cosnt map = getOrCreateWeakMapEntry(source); // pseudo code
return map.get(value) || map.create(value, { value });
} Downside - on items update all "pickers" will be reset as well as data stored in the combine function. But probably this is what is needed. In other words - even if the result seems a little less readable - what if we can resolve a few technical problems by introducing extra functionality and enforcing it via TypeScript types (all selectors used in selectors has to return object) or some babel magic. 👉 The ability to memoize is bound to the ability to forget, and Rust got it right – https://doc.rust-lang.org/1.9.0/book/lifetimes.html |
Beta Was this translation helpful? Give feedback.
-
Hi, @markerikson ! https://github.com/vlanemcev/reselect-debugger-flipper Here`s the PR to add this debugger into Reselect README: Thanks! |
Beta Was this translation helpful? Give feedback.
-
Reselect 4.1 is now available, and addresses most of the concerns listed here. It adds a configurable cache size to |
Beta Was this translation helpful? Give feedback.
-
The below challenge is a result of using redux-toolkit, which leverages reselect. Since Reselect doesn't offer its own react hooks, hooks like useSelector are implemented one level higher. There's a large jump in complexity from RTK's I'd like to see reselect own the I'm going to make three assumptions here
That said, the following code is for the combiners pattern, called Like Hook Exampleimport {useState} from "react";
import {useDerivedSelector} from "redux-toolkit"; // from "reselect/react"
const MyComponent = () => {
const [items, setItems] = useState();
const derivedValue = useDerivedSelector(selectFoo, (foo) => {
// combine foo and items here
}, [items]);
} ConsequencesHooks Across Redux Ecosystem Right now, the majority of hooks are in redux-toolkit. I don't think this changes, it just makes some hooks available in the lower level library closer to their relevant concerns. It does change exports from the module(s) though, and individuals using both Reselect and RTK might be confused as to which import to use. Encourages Pure Selectors This pattern makes it more obvious that Encourages Selectors Aside Slices This pattern also encourages the pure selectors to live next to slices created by RTK. In theory, a set of Pure selector functions can also be auto-generated from the Sample Hook// hypothetical hook code, written quickly to show the idea in practice
const useDerivedSelector = (...args) => {
const deps = Array.isArray(args[args.length -1]) ? args.pop() : [];
const combiner = useRef(args.pop());
const result = useMemo(() => {
combiner.current(...args);
}, [...args, ...deps])
} In practice, I wouldn't use the splatted args, and I'd add types for the hook. There is no need for an equality function in this example, as we're relying on React's reconciler to determine if the combiner must be reran based on the selector results. Finally, none of this code prevents us from using more complex |
Beta Was this translation helpful? Give feedback.
-
Resurrecting this discussion thread: I filed this two years ago. That led to me doing a bunch of work to put out Reselect 4.1 in late 2021, including applying a new set of TS types that used newer TS inference abilities to extract types from the input selectors and determine the final selector arg types, and new options for We're starting work on Redux Toolkit 2.0, and that will likely mean bumping major versions for Given that, it makes sense to review Reselect and consider what further improvements we could make in a v5, including completely breaking API changes. So, similar to the original questions:
|
Beta Was this translation helpful? Give feedback.
-
Hey Mark, Thanks for looking into this. As I mentioned on twitter, it would be nice to have a version of (copied from rtk docs) interface MyData {
// ...
}
interface MyKnownError {
errorMessage: string
// ...
}
interface UserAttributes {
id: string
first_name: string
last_name: string
email: string
}
const updateUser = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
UserAttributes,
// Types for ThunkAPI
{
extra: {
jwt: string
}
rejectValue: MyKnownError
}
>('users/update', async (user, thunkApi) => {
const { id, ...userData } = user
const response = await fetch(`https://reqres.in/api/users/${id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
body: JSON.stringify(userData),
})
if (response.status === 400) {
// Return the known error for future handling
return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
}
return (await response.json()) as MyData
}); I would love to have something similar with const selectUserById = createSelectorWithParameters<
// type of root state
RootState,
// Type of the first argument to the selector creator function
number
>(
// select slice
(state) => state.main,
// select users
(main) => main.users,
// select specified user
(users, id) => users.filter(val => val === id)
); which would work something like this: // App.jsx
function MyComponent() {
const user = useSelector(selectUserById(3));
// ...
} |
Beta Was this translation helpful? Give feedback.
-
While we're at it... I always felt that slices were an almost-there solution. Reducers are aware only of the portion of state that is the current slice (rather than the root of the store, which is defined higher up than the slice module), but selectors have to know where in the store the current slice lives. I get why, but it always felt wrong. I would love it if createSelector (when called from within a userSlice) was NOT required to have Rather than this: const selectUserAddress = createSelector(
(state: RootState) => state.user,
(user: UserState) => user.address
); We would have this: const selectUserAddress = createSelector(
(user: UserState) => user.address
); |
Beta Was this translation helpful? Give feedback.
-
Posting about performance problems in Reselect in response to this. I first want to say that problems like this are very rare. Reselect is plenty fast for most situations. You have to have some crazy amounts of heavy data to hit the hard limit. Here's a little insight into how we did: The StageThe company I work for develops a bond trading platform. We deal with updates over websockets to the tune of thousands per second. This requires advanced async flow management using RxJS to throttle and buffer updates before they hit the state management layer. In React, it also requires precise control over rerenders. In Redux, that means Reselect naturally came into play a lot. We aggregate as much data as possible on the server before streaming to the UI, but some things aren't practical to handle that way. For these, we had a big graph of Reselect selectors pulling data from various data streams and aggregating them on the fly. The ProblemWe can't buffer updates for too long (about 2-3 seconds was the hard limit for most data streams):
But if we flush updates too often, too many calculations have to run too often:
Long story short, Reselect was the bottleneck - not Reselect's own code, but the code we had to put in selectors and the frequency with which we needed Reselect to run it. We came to call it a thread hogger. 90% of our time spent improving performance was spent working around Reselect. We ultimately moved away from Redux 2 years ago. Reselect is the main reason why. The InterludeBefore I get to the fix, I want to say a few things: First, I personally am a big fan of Redux. I've used it a lot, love the concepts and theory, and generally enjoy working with it. Second, just to reiterate, this is an extreme situation. If I was maintaining Reselect, I probably wouldn't change a thing after reading this comment. The root of these problems is more fundamental to the state model - beyond Reselect's control. Take this more as an FYI if you're curious to know when and how the model breaks down and what we did to fix it. Third, I am the bad guy in this story: The FixIn 2020, I proposed a new model for state management patterned after Recoil and Jotai, but much more performant, flexible, and easy to migrate to incrementally. I don't want to hijack this thread with it. If you're curious, it was actually publicly released earlier this week and you can check it out here. After 3 months of development, we started plugging it into our apps. It fixed many problems, but here are the pieces relevant to what I've mentioned: Modular StateWith atoms, state is naturally broken up, meaning we can flush more stuff with less overhead. While each individual atom has more overhead than a state slice in Redux, atoms can scale almost indefinitely since each one is completely isolated/modular/autonomous. // hits thousands of reducers/useSelector subscriptions
reduxStore.dispatch(action)
// hits only this atom and its dependents in the atom graph (inc. components)
atom.dispatch(action) This is not as big a deal as it sounds - despite Redux's global approach, it's still very performant. But this did help a little and, more importantly for us, it will continue to scale beyond what we'll ever need. Selector Buffering/ThrottlingAtoms and selectors use the same graph. This means that at any point in a selector tree, you can bump down to an atom to grab complete control over memoization and async flow. We use this power to selectively buffer or throttle updates via RxJS at certain points in the selector tree only where they're needed. This means we can flush updates often, keeping most of the UI snappy while only deferring updates in specific places where needed. It also removed backpressure problems and lots of ugly code managing it. I'll use code from our library to demonstrate. You should be able to see how this equates to a similar Redux/Reselect setup, but let me know if anything is unclear. // In practice, each entity would get its own atom. And yes, you'd normalize the
// data structure. But for demo purposes:
const entitiesAtom = atom('entities', () => ({
fruits: {
apples: ['pink lady apple', 'granny smith apple'],
lemons: ['lisbon lemon', 'eureka lemon'],
limes: ['key lime', 'kaffir lime'],
oranges: ['navel orange', 'mandarin orange'],
},
}))
const getFruits = ({ get }) => get(entitiesAtom).fruits
const getApples = ({ select }) => select(getFruits).apples
const getLemons = ({ select }) => select(getFruits).lemons
const getLimes = ({ select }) => select(getFruits).limes
const getOranges = ({ select }) => select(getFruits).oranges
const getCitrusFruits = ({ select }) => [
...select(getLemons),
...select(getLimes),
...select(getOranges),
]
const getSortedCitrus = ({ select }) => select(getCitrusFruits).sort() I'll stop there for the sake of brevity, but imagine this selector graph growing to 10x this size, full of many expensive operations - filters, maps, sorts, object spreads, Immutable With atoms, any selector can be turned into an atom fairly easily. // turn the above `getCitrusFruits` selector into an atom (ion is just an atom
// specially designed for selection operations):
const citrusFruitsAtom = ion('citrusFruits', ({ select }) => {
const store = injectStore()
const selectors = [getLemons, getLimes, getOranges]
// subscribe to updates in upstream selectors, but discard results here
selectors.forEach(selector => select(selector))
// a custom injector that hides implementation details. See below if curious
injectThrottle(() => {
store.setState(selectors.flatMap(selector => select(selector)))
})
return store
}) (ignore me, I'm just some implementation details for the above example)Here's an example of how const injectThrottle = updateState => {
// set initial state on first run
if (!injectWhy().length) updateState()
// injectors are like hooks for atoms. This works just like `useMemo` ...
const subject = injectMemo(() => new Subject(), [])
subject.next('update happened!')
// ... and this works just like `useEffect`:
injectEffect(() => {
// use RxJS to throttle updates, only updating every 2 seconds
const subscription = subject.pipe(throttleTime(2000)).subscribe(updateState)
return () => subscription.unsubscribe()
}, [])
} Full codesandbox demonstrating this here. Apologies for making this section so long, but it was the feature that benefitted us the most. This capability fixed everything but our most difficult table. For that we needed a serious escape hatch: Action StreamsThis is a "don't push the big red button" type of escape hatch, but we needed it in our most extreme table. I won't go into too much detail here, but basically our stores can be consumed both as streams of state and streams of actions. When hooking into an action stream to perform DOM updates, you skip React and the reactive paradigm completely, giving you a big performance boost at the cost of very brittle code (we love reactivity for a reason!). This is obviously a big antipattern and I don't want to encourage it, so I'll stop there. The FinaleSo what can Reselect get out of this? Well, as I said, probably not much without some major overhauls that are beyond the scope of Reselect itself. But at the very least, I hope you learned something about managing state derivations in the extremes. Happy to hear if anyone did manage to glean some insights from this that could apply to Reselect. Let me know if anything needs clarifying. If anything does come out of this, I'm willing to contribute to discussions and possibly even help with implementing changes or at least writing documentation or articles to help educate on this matter. Cheers 🥂 1 I believe Reselect's |
Beta Was this translation helpful? Give feedback.
-
A little offtopic, but what problem we solve? It's some sort of mnemonic to clarify the problem. And we try to solve many different problems:
One of the biggest problems of reselect is cache size and it does not have a direct solution except WeakMaps, which are nowhere to store due to the ever-changing immutable nature of redux. Unless we can store cache in redux itself. This is less about what we could do, more about the idea of looking for a problem not for
|
Beta Was this translation helpful? Give feedback.
-
General update for folks: Per reduxjs/redux-toolkit#958 (comment) , my current goal is to get RTK 2.0 out the door just as soon as possible. RTK 2.0 will include Redux core 5.0 with updated TS types, and also include Reselect 5.0. (React-Redux 9.0 will be going out simultaneously.) Those package updates are primarily focused on modernizing our published ESM/CJS compatibility, but have some tweaks around removing deprecated APIs, improving TS types, etc. For Reselect 5.0 specifically: I'm still not making any massive changes to
Right now all I want is to get the RTK 2.0 release wave done so that folks can use the updates we've got, without major breaking changes. We've already deferred any RTK Query feature changes to after RTK 2.0, including being willing to publish RTKQ-specific breaking changes in a follow-on RTK 3.0. Similarly, I'm still open to future Reselect API design changes in a future Reselect 6.0, I just don't have time to meaningfully consider anything right now and don't want that sort of design work to block getting RTK 2.0 out the door. So. I know there's a lot of discussion in this thread. We've addressed the initial "hard to customize" concerns, and some of the cache size concerns. I'm still up for future improvements down the road! |
Beta Was this translation helpful? Give feedback.
-
Update: Reselect 4.1 Now Available!
Reselect 4.1 is now available, and addresses most of the concerns listed here. It adds a configurable cache size to
createSelector
, optional result equality checks to address thetodos.map(todo => todo.id)
use case, completely rewritten TS types, and much more! See the release notes for details:https://github.com/reduxjs/reselect/releases/tag/v4.1.0
Original Text
Reselect has been loosely maintained over the last couple years. There's been a lot of PRs filed that have been sitting around, including some that require a new major version. The goal of this discussion is to:
I'd like to thank @ellbee, who's been the primary maintainer. Real life has taken up his time lately, so he's given myself and @timdorr publish rights on NPM and a green light to work on PRs.
I already have more than enough on my todo list with the rest of the Redux family of libraries, so I need to limit my involvement in maintaining Reselect. However, I'm happy to help shepherd the conversation here, define some vision, and provide guidance.
I'd love to see some folks in the community volunteer to help do whatever work's needed here, and even come on board as an active maintainer for Reselect.
Prior Reselect v5 Planning
In a discussion with @ellbee earlier today, he said:
So, as a starting point, it seems reasonable to assume that we'd rewrite Reselect's source to TypeScript, update the types to work better with variadic args, and try to address known pain points and additional use cases.
Current Reselect Pain Points and Problems
Cache Size and Selector Instance Reuse
These two problems go hand-in-hand. Reselect only has a cache size of 1 by default. This is fine when a selector is only being given
state
as its only argument. However, it's very common to want to reuse a selector instance in a way that requires passing in varying arguments, such as a "filter items by category" selector:In cases like this, multiple components all call the same selector with different arguments one after the other. So, it will never memoize correctly.
The current workaround here, when used with React-Redux, is to create unique selector instances per component instance. With
connect
, this required a complex "factory function" syntax formapState
:With function components, this is a bit less obnoxious syntax-wise, but still annoying to have to do:
Clearly this is a major use case that is difficult to work with right now.
It's possible to customize Reselect's caching behavior by calling
createSelector(customMemoizer)
, but that's an extra level of complexity as well.Optimizing Comparison Behavior
Reselect works by:
However, the use of shallow equality / reference checks here can lead to calculating a new output result in cases where it wasn't truly necessary. Take this example:
This recalculates the result any time the
todos
array changes. However, if wedispatch(toggleTodo(3))
, we create a new todo object andtodos
array. That causes this selector to recalculate, but none of the todo descriptions changed. So, we end up with a newdescriptions
array reference even though the contents are shallow equal. Ideally, we'd be able to figure out that nothing really changed, and return the old reference. Or, even better, not even run the final calculation, because it might be relatively expensive. (Issue ref: #451)Related to this, it's also possible to write poorly-optimized selectors that have too broad an input (such as using
state => state
as an input selector) and thus recalculate too often, or may just not be well memoized.Finally, Reselect doesn't do anything to help with the output itself taking a long time to calculate (Issue ref: #380 ).
Debugging Selector Recalculations
Reselect was made to work with selectors acting as inputs to other selectors. This works well, but when multiple selectors are layered on top of each other, it can be hard to figure out what caused a selector to actually recalculate (see the selectors file from the WebAmp project as an example).
Other Issues
createSelector(input1, input2, output)
) was bad for TS usage previously. This might not be an issue now with TS 4.x.undefined
selector (which can happen due to circular imports)Existing Ecosystem Solutions and Addons
Open Reselect PRs
There's a bunch of open PRs that are trying to add various small changes in functionality and behavior. Some relevant ones:
defaultMemoize
memoizedResultFunc
so you can clear itdefaultMemoize
resultCheckMemoize
#456: adds aresultCheckMemoize
memoizer that can be used instead ofdefaultMemoize
Ecosystem: Caching
There are a bunch of different packages that either wrap Reselect directly, or implement similar behavior separately.
The biggest one is https://github.com/toomuchdesign/re-reselect , which specifically creates a customized memoization function that supports multiple cached keys so that one selector instance can be reused in multiple places.
Meawhile, @josepot came up with an approach for keyed selectors, submitted it as #401 , and also published it as https://github.com/josepot/redux-views .
There's also https://github.com/ralusek/reselectie , which is an alternative lib with a similar API.
Ecosystem: Comparisons
The best option I found for dealing with cases that return arrays and such is https://github.com/heyimalex/reselect-map , which has specialized wrappers like
createArraySelector
that deal with one item at a time.Ecosystem: Debugging
The biggest piece here is https://github.com/skortchmark9/reselect-tools , which adds a wrapper around
createSelector
that tracks a dependency graph between created selectors. It also has a really neat browser DevTools extension that visualizes that dependency graph.While searching NPM for Reselect-related packages, I also ran across:
createSelector
to keep some stats on call durationsAlternative Selector Libraries
There's also other selector-style libraries with varying approaches and APIs:
The one I find most intriguing is https://github.com/dai-shi/proxy-memoize. @dai-shi has been doing amazing work writing micro-libs that use Proxies. I think that
proxy-memoize
actually does solve some of Reselect's pain points, and I want to start officially recommending it as another viable option. I suggest reading reduxjs/react-redux#1653 , which has discussion between myself, @dai-shi, and @theKashey regarding howproxy-memoize
works and whether it's sufficiently ready.@theKashey previously wrote https://github.com/theKashey/kashe , which uses WeakMaps to do the caching behavior.
https://github.com/taskworld/rereselect and https://github.com/jvitela/recompute both use their own internal forms of observables to track dependencies and updates.
https://github.com/pzuraq/tracked-redux uses the Ember "Glimmer" engine's auto-tracking functionality to provide a tracked wrapper around the Redux state.
Ecosystem: Library Summaries
Since I was researching this, I threw together a table with some of the more interesting selector-related libs I found. Some are wrappers around Reselect, some are similar to Reselect API-wise, and some are just completely different approaches to sorta-similar problems:
createSelector
to track debugging statsConclusions
Reselect is Widely Used
For reference, Github shows 1.4M+ repos depending on Redux, and 400K+ depending on Reselect. So, any changes we make should try to keep the API similar to minimize breakage.
Biggest Issue: Caching and Output Comparisons
This seems like the main problem people are concerned about and is the biggest annoyance working with Reselect right now.
Reselect Should Be Updated Even If Other Options Exist
I really like how
proxy-memoize
looks and I think it's worth us promoting it officially. That shouldn't stop us from improving Reselect while we're at it.Rewrite Reselect in TypeScript
We might as well unify the code and the types so they don't get out of sync, and start building Reselect against multiple versions of TypeScript.
Coordinate on API Tweaks
There's a bunch of overlapping PRs with small tweaks, and we should try to figure out a coordinated and coherent approach to updating things vs just randomly merging a few of them.
Final Thoughts
So, here's the questions I'd like feedback on:
I'd like to tag in a bunch of folks who have either contributed to Reselect or are likely to have relevant opinions here:
@ellbee, @timdorr, @josepot, @OliverJAsh, @dai-shi, @theKashey, @faassen, @Andarist, @eXamadeus
I'd like to get feedback from them and the rest of the Redux community!
I'd specifically recommend reading through the "Reselect v5.0" proposal by @josepots and the
proxy-memoize
discussion over in the React-Redux issues as background for this.Beta Was this translation helpful? Give feedback.
All reactions