Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Reselect v5 Roadmap Discussion: Goals and API Design#491

markerikson started this conversation inIdeas
Discussion options

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 tocreateSelector, 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:

  • List what use cases Reselect v4 does not cover, and what pain points users have with it
  • Compare Reselect's APIs with other similar libraries in the ecosystem and identify capabilities that would be worth borrowing
  • Nail down a desired set of capabilities for Reselect v5 and determine what the API should look like
  • Add any discussion necessary to figure out implementation details

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:

The version 5 release was just going to be fixes to the TypeScript bindings resolving the problem where we had to use overloads so there was a maximum number of parameters that could be typed correctly. As I recall we tried changing the position of the combining function and enforcing that dependencies had to be given as an array, which would have been a problematic breaking change so were also going to look into the feasibility of a code mod. I think the whole thing is moot now with variadic tuple types in TypeScript 4.0 and it looks like people are actively working on it at the moment.

The other thing that was being considered was#401

Oh, and it was going to be written in TypeScript to side step the whole where should the bindings live issue

I do think that josepots approach in the linked issue is good, but I wonder if reselect should be deprecated rather than fundamentally alter how it works. I worry about the amount of projects using it in its current form and don’t want it to be the cause of lots of (potentially subtle) breakage or performance problems.

[To clarify]: I just mean trying to move it away from being the default/recommended option, especially now that es6 is so widely supported and things like proxies are feasible to use.

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 givenstate 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:

constselectItemsByCategory=createSelector(state=>state.items,(state,category)=>category,(items,category)=>items.filter(item.category===category))selectItemsByCategory(state,"a");// first call, not memoizedselectItemsByCategory(state,"a");// same inputs, memoizedselectItemsByCategory(state,"b");// different inputs, not memoizedselectItemsByCategory(state,"a");// different inputs from last time, not memoized

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. Withconnect, this requireda complex "factory function" syntax formapState:

constmakeMapState=(state)=>{constselectItemsForThisComponent=makeUniqueSelectorInstance();returnfunctionrealMapState(state,ownProps){constitems=selectItemsByCategory(state,ownProps.category);return{items}}};

With function components, this is a bit less obnoxious syntax-wise, but still annoying to have to do:

functionCategoryItems({category}){constselectItemsForThisComponent=useMemo(makeUniqueSelectorInstance);constitems=useSelector(state=>selectItemsForThisComponent(state,category));}

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 callingcreateSelector(customMemoizer), but that's an extra level of complexity as well.

Optimizing Comparison Behavior

Reselect works by:

  • Passing all parameters to all "input selectors"
  • Saving all the input selector results to an array
  • Checking to see if any input results changed by reference
  • If so, passing all input results to the output selector

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:

constselectTodoDescriptions=createSelector(selectTodos,todos=>todos.map(todo=>todo.text))

This recalculates the result any time thetodos 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 usingstate => 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 (seethe selectors file from the WebAmp project as an example).

Other Issues

  • The TS typings are very complex, and also possibly broken as of TS 3.1 for some cases
  • The use of the multi-argument form (createSelector(input1, input2, output)) was bad for TS usage previously. Thismight not be an issue now with TS 4.x.
  • People want to be able to customize more of the API, including additional memoization checks and resetting cache
  • Also better error handling, like detecting anundefined 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:

Ecosystem: Caching

There are a bunch of different packages that either wrap Reselect directly, or implement similar behavior separately.

The biggest one ishttps://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 ashttps://github.com/josepot/redux-views .

There's alsohttps://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 ishttps://github.com/heyimalex/reselect-map , which has specialized wrappers likecreateArraySelector that deal with one item at a time.

Ecosystem: Debugging

The biggest piece here ishttps://github.com/skortchmark9/reselect-tools , which adds a wrapper aroundcreateSelector 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:

Alternative Selector Libraries

There's also other selector-style libraries with varying approaches and APIs:

The one I find most intriguing ishttps://github.com/dai-shi/proxy-memoize.@dai-shi has been doing amazing work writing micro-libs that use Proxies. I think thatproxy-memoize actuallydoes solve some of Reselect's pain points, andI want to start officially recommending it as another viable option. I suggest readingreduxjs/react-redux#1653 , which has discussion between myself,@dai-shi, and@theKashey regarding howproxy-memoize works and whether it's sufficiently ready.

@theKashey previously wrotehttps://github.com/theKashey/kashe , which uses WeakMaps to do the caching behavior.

https://github.com/taskworld/rereselect andhttps://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:

RepoTypeDomainNotes
https://github.com/toomuchdesign/re-reselectWrapperCache sizecache size and varying inputs, mapped by cache key
https://github.com/heyimalex/reselect-mapWrapperComparisonsspecialized selectors to handle arrays/objects
https://github.com/theclinician/selectorsWrapperComparisonsVarious wrapper selectors for mapping over collections
https://github.com/techstack-nz/reselect-lensWrapperDebuggingCreates a Redux store with selectors as "reducers" for DevTools viewing
https://github.com/AaronBuxbaum/analyze-reselectWrapperDebuggingWrapscreateSelector to track debugging stats
https://github.com/skortchmark9/reselect-toolsWrapperDebuggingCreates graph of selectors; has its own DevTools UI
https://github.com/trufflesuite/reselect-treeWrapperOutput structurecreate trees of selectors with deps between leaves
https://github.com/liitfr/relational-reselectWrapperOutput structureConstructs joins between state sections
https://github.com/jvitela/recomputeCompetitorCache sizecustom observables; unbounded cache size, any num args, can be shared across components
https://github.com/theKashey/kasheCompetitorCache sizerelies on WeakMaps
https://github.com/ralusek/reselectieCompetitorCache size"Smaller and faster"; has cache key parameters
https://github.com/josepot/redux-viewsCompetitorCache sizeNearly API-compat. Created to inspire Reselect 5.0
https://github.com/taskworld/rereselectCompetitorInputsdynamic dependency tracking instead of static; debug introspection
https://github.com/spautz/dynamic-selectorsCompetitorInputsDynamic selector construction; also can wrap Reselect selectors
https://github.com/pzuraq/tracked-reduxAlternativeTrackingUses Glimmer's tracking with Proxies
https://github.com/dai-shi/proxy-memoizeAlternativeTrackingUses custom Proxies for tracking dependencies
https://github.com/theKashey/beautiful-react-reduxAlternativeTrackingWraps React-Redux; wraps and double-calls your mapState, tracks deps
https://github.com/dai-shi/reactive-react-reduxAlternativeTrackingUses Proxies to track state access; also seereduxjs/react-redux#1503

Conclusions

Reselect is Widely Used

For reference, Github shows 1.4M+ repos depending on Redux, and 400K+ depending on Reselect. So, any changes we make shouldtry 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 howproxy-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:

  • What use cases does Reselect not cover sufficiently now?
  • What other improvementscan be made to Reselect?
  • What other changesshould be made to Reselect?
  • What other pain points have you run into?
  • What should a final Reselect v5 API design look like?

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 themand the rest of the Redux community!

I'd specifically recommend reading throughthe "Reselect v5.0" proposal by @josepots andtheproxy-memoize discussion over in the React-Redux issues as background for this.

You must be logged in to vote

Replies: 15 comments 32 replies

Comment options

Thanks for putting this together@markerikson! Really hope this gains some traction.

You must be logged in to vote
0 replies
Comment options

1.4M+ repos depending on Redux, and 400K+ depending on Reselect.

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:

  • autocreating selectors likemakeUniqueSelectorInstance, or autoscoping reselect internal cache to the current component.
  • something about wrapping/upwrapping proxies

For the cases without Redux Reselect can look towards atomic state managers like:jotai orreatom, as they all are aboutA+B -> C transformation

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.

You must be logged in to vote
1 reply
@markerikson
Comment options

markeriksonFeb 17, 2021
Maintainer Author

Just Reselect. Granted, we can assume that the vast majority of Reselect usage is with Redux state (and likely React-Redux as well), but I'm not planning to add any special APIs or behavior to React-Redux or Redux Toolkit that would be specific to Reselect.

So, the goal for this discussion is: given Reselect v4 as a starting point, and this list of pain points and other ecosystem libs, what improvements to Reselect's API and implementation should we be aiming for?

Comment options

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 byuseSelector.

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 inuseSelector, that the result is referentially equal when the data is the same, to prevent excessive re-renders. This means I care more about being able to memoize based on a deep equality check on outputs than I do about memoizing inputs – which may not be easily possible (if your input state cannot be mapped to referential equal inputs for your combining selector).

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.

constselectFiles=state=>state.files;constselectFileNames=createSelector([selectFiles],files=>files.map(file=>file.name));

If I useselectFileNames inuseSelector, the component will re-render when an action is received that updates any files' contents. This is a known problem that you've discussed above. Now, itis possible to usecreateSelectorCreator to fix the above problem, by creating a version ofselectFiles that memoizes based only on thename property of each file, and then using that as the input selector toselectFileNames.

I.e, something like this

constcreateCompareEqualForKeySelector=(keySelector:(_:unknown,__:unknown,key:any)=>boolean|undefined)=>(currentEntities:any,previousEntities:any)=>{if(currentEntities.length!==previousEntities.length){returnfalse;}for(letidx=0;idx<currentEntities.length;idx++){constcurrentEntity=currentEntities[idx];constpreviousEntity=previousEntities[idx];if(!isEqualWith(currentEntity,previousEntity,keySelector)){returnfalse;}}returntrue;};constcreateFieldSelector=<T,FextendskeyofT>(selector:(state:any)=>T[],field:F)=>createSelectorCreator(defaultMemoize,createCompareEqualForKeySelector((curr,prev,key)=>!key||field===key ?undefined :true)constselectFiles=state=>state.files;constselectFileNames=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 forcreateSelectorCreator. As you've outlined above, there are PRs to do that potentially even without a breaking change.

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 isnot 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 thecreateSelector function which you may find you want to in the future. I generally think functions that take variadic arguments where it treats items differently based on their position are an anti-pattern and tend to cause more problems than they're worth. If you're going to make a breaking change, save yourself the maintenance headache and remove variadic args.

Just some musings on my thoughts – I don't know how valuable they are. Sorry it's taken so long to get these out!

You must be logged in to vote
7 replies
@markerikson
Comment options

markeriksonMar 20, 2021
Maintainer Author

Great thoughts from all of you! :)

I think at this point we've described the problem space pretty well. So, what I'm really interested in is some concrete suggestions for changes to the API and feature set, including thoughts on what could go into a 4.x release and what should go into a new 5.0 major.

Per the "Reselect dev tools" bit, thereis actually such a thing:https://github.com/skortchmark9/reselect-tools , which I linked above in the "Debugging" section. I haven't actually used it myself yet, but it seems pretty neat, and perhaps it would be worth trying to coordinate with the author to get that added in to the core somehow.

@Soundvessel
Comment options

reselect-tools looks initially useful but being I tend to normalize and use re-reselect for entity data I would be hard-pressed to use something that doesn't cover all the selectors I am using. reselect-tools does have anopen request to add re-reselect support.

Before the integration of dev tools into the core, I would want to understand the performance impact of the said tools. An easy way to turn it on and off would be nice since I don't want it always affecting perceived performance as I build.

Re-reselect in particular solved the biggest gripe I had with reselect's API which was the factory functions, especially the one required for the componentmapState. With 'createCashedSelectormy component'smapState` can stay consistent when moving from one to many cache sizes. IMO caching should be narrowed to the selector so the component doesn't care.

I think one drawback of the current API is that we don't take advantage of our knowledge of the inputs. We currently don't distinguish them when creating the selector which leads to more boilerplate after the fact such as with re-reselect's resolver function.

For myself, there are 3 types of selectors, inputs, that cover a majority of my use cases and could possibly automate much of the cashing I require without the need for a cache resolver function. Wild idea but what if we used arrays to identify these types?

For example:

constselectFiles=state=>state.files// shouldn't affect cache size, let's call it a "standard input"constselectCurrentUserId=createSelector(selectCurrentUser,// in state, not from propuser=>user.id)// here we want cache based on the result, let's call it a "cache result input"constselectPropsFileId=(state,props)=>props.fileId// prop selector, another example of "cache result input"constselectFileId=(state,fileId)=>fileId// straight up input but not from prop, let's call it a "cache input"// although personally I would make this like the prop selector for composition, let's call it a "cache input"// what if an API could allow us to identify these types and cache appropriately?constselectFileAuth=createSelector(// use array args to identify caching strategy?[selectFiles],// array of standard inputs (selectors)[selectCurrentUserId],// array of cache result inputs (selectors)[selectFileId],// array of cache inputs (selectors)// order in the result function remains unchanged(files,fileId,currentUserId)=>files[fileId][currentUserId])// generates and uses`${fileId}-${currentUserId}` as cache key,// multiple input/result cache selectors are simply concat with dashes?// Error when not string/number signaling you need to use cache resolver function instead?// same example but with the prop selector for fileIdconstselectFileAuth2=createSelector([selectFiles],[selectPropsFileId,selectCurrentUserId],(files,fileId,currentUserId)=>files[filed][currentUserId])// generates and uses`${fileId}-${currentUserId}` as cache key// with prop selectorsconstselectFileAuth3=createSelector([selectFiles][selectPropsFileId,selectPropsUserId],(files,fileId,userId)=>files[fileId][userId])// generates and uses`${fileId}-${userId}` as cache key// or when you want to break it up because more than one selector can take advantage of the file entity caching such as being fed into different selectors for different viewsconstselectFile=createSelector([selectFiles],[selectPropsFileId],(files,fileId)=>files[fileId])// fileId as cache keyconstselectFileAuth=createSelector([selectFile],[selectPropsUserId],(file,userId)=>file[userId])// userId as cache key

This would remove a lot of the repeated work I do with re-reselect's resolver function API. Granted we would still need access to create a cache resolver function if the caching couldn't be determined by a number/string input/result of a selector.

@theKashey
Comment options

I would strongly encourage not to follow re-reselect API because it creates memory leaks as there is no "clean" condition.
For React there is only one "stable" way of making per-something memoization and it's "useMemo". In "quotes" because it's just about storing result in a component's fiber and cleaning up with the fiber as well. Here is anarticle to read.

In other words, re-select functionalityhas to be implemented on auseSelector level.
Technically speaking@josepot'sredux-views are working on this principle via placingselector.use intouseEffect

@adamerose
Comment options

+1 on the devtools point. I would really like to see Reselect be integrated with Redux DevTools and have selectors shown alongside state. Not being able to see my derived state through debug tools is a big pain point for me when using Redux compared to Overmind and MST.

There is an issue here about this with many upvotes but discussion died out in 2018.
reduxjs/redux-devtools#352

perhaps it would be worth trying to coordinate with the author to get that added in to the core somehow.

@markerikson

The author left a comment in that issue above saying that they would love to work on integrating into redux devtools and that was their plan all along. Not sure if they still have the bandwidth or interest but maybe you can reply and revive the discussion.

I haven't actually used it myself yet

I'm curious then what is your debug process when you see unexpected behavior related to app state and want to confirm that your derived state looks as expected? Seeing selector values feels like an important debug step to me

Also probably worth considering dev tool support forproxy-memoize if that's being recommended as a Reselect alternative in the official docs.

@markerikson
Comment options

markeriksonNov 4, 2021
Maintainer Author

@adamerose : I usually just look at the output value in a debugger or console log.

I think this would be a useful idea to explore, although the first question would be how to expose debugging info from the selectors. This would likely require looking athttps://github.com/skortchmark9/reselect-tools and figuring out how to port that functionality into Reselect itself.

I've got enough stuff going on I don't think I could tackle that personally, but if you or someone else would like to look into it that would be great!

Comment options

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. Checkthis example. One of the solutions is allowed to call a selector inside a reducer. Here some related project -topologically-combine-reducers,reatom.

Also, I thinklimitations about standard Map/Set inproxy-memoize is incompatible with possible development flow and should not be used as a default.Impossible to memoize nested selectors looks to limiting too. I solve the last problemthat way (test). Maybe,proxy-memoize may solve it too, cc@dai-shi

You must be logged in to vote
1 reply
@dai-shi
Comment options

Hmm, I'm not sure if my descriptions give you the right understanding. Or, maybe I'm confused. Didn't even know reatom is proxy based. or, not...?

Comment options

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'stweet lit a fire under my butt to finish off this post.

Update on type changes

I'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 TypeScript

I'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.

The TS typings are very complex, and also possibly broken as of TS 3.1 for some cases

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 Reuse

This is a fascinating problem. There are many good suggestions to solve this already.

I personally thinkre-reselect has already solved this issue quite cleanly. Obviously, this isn't a solution coming fromreselect itself, but this pattern is quite well accepted in the community and in practice. Re-reselect addresses the following issues pretty cleanly:

  • cache size and selector instance reuse
  • TS typings would less complex than "key selector" suggestions (like the ones discussed here:Version 5.0.0 proposal #401)
  • simple declarative UI is kept in place
  • "internal API" helpers aren't necessary (defaultMemoize andcreateSelectorCreator)

It might be worth discussing either incorporating or absorbing this functionality intoreselect itself. Maybe@toomuchdesign would be able to comment on how feasible this solution might be?

Optimizing Comparison Behavior

Ithink this could be solved with a solution inspired byre-reselect'screateCachedSelector calledcreateCachedResultSelector. This is just a quick thought, but something like this, might work:

const selectTodoDescriptions = createCachedResultSelector(  selectTodos,  todos => todos.map(todo => todo.text))(  // instead getting the selector arguments like `createCachedSelector`,  // this would get the previous and next result values as input to the  // memoizer function  (previous, next) => {      // here previous and next would be the cached value and the "new"      // array value, then just return whichever one is "correct"  })

Some default "result memoizers" could be provided out of the box. Something likeshallowArrayMemoizer (written out to be clear, not necessarily efficient):

exportconstshallowArrayMemoizer=<T>(prev:T[],next:T[]):T[]=>{constsameLength=prev.length===next.lengthconstsameValues=sameLength&&prev.every((val,idx)=>val===next[idx]);returnsameValues ?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 Recalculations

This 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

  • rewrite in TS (for lots of reasons)
  • integratere-reselect into the library
  • add some extra helpers (a lacreateCachedResultSelector) along with the previous suggestion
You must be logged in to vote
2 replies
@markerikson
Comment options

markeriksonMar 21, 2021
Maintainer Author

Great feedback!

Yes, I'm fully intending that v5 should be in TS at this point. No reason not to.

I'd also be inclined to pull the trigger on dropping variadic arguments and requiring the array format for input selectors. I think that might make it easier to add some kind of an options object as the last argument. In fact, if would be nice if the options object could be used for things like supplying memoization parameters, and we could avoid having a dozen variations ofcreateSelector. Like, hypothetically:

constselectSomething=createSelector([inputSelector1,inputSelector2],outputSelector,{cacheSize:10,cacheKey:(arg1,arg2)=>arg1.id,outputComparison:shallowArrayMemoizer})

FWIW, I see that@josepot has already created a codemod that would convert uses ofcreateSelector to the array format:

https://github.com/josepot/redux-views/blob/master/transforms/to-array-dependencies.js

@eXamadeus
Comment options

With "updated" typescript definitions, variadic support isn't really problematic. I do think a decision should just be made and go in one direction over the other. Supporting both isn't really necessary.

Array support is probably the cleaner approach. If I had to pick one, that would be my vote. I really like the suggestion you provided. The options object as a third argument would be cleaner than curried functions and would support aton of features simultaneously.

Comment options

I've read this conversation once again, and look like there are 3 different use cases we are trying to solve:

  • structured selectors for a better abstraction. Just ancommon language between your frontend code and your state.
  • combiners where two or more elements meet to produce another one
  • pickers - the actual "state access" operation

And then let's look at this separation from WeakMap based cache control (like kashe), where "single cache entry" is replaced by WeakMap

  • pickers always pick an "untransformed" object and no extra logic is required
  • combiners usually have a "primary data source" and a "key", one can use apicker to pick a data element from data source and then do necessary operations. That data element (+ selector) forms an unique reference.
  • structured selectors follows thecombiners logic.

Let's try to solve a few examples:

constselectTodoDescriptions=createSelector(selectTodos,todos=>todos.map(todo=>todo.text))
  • selectTodos is apicker
  • selectTodoDescriptions is just derive one view from another view - astructured selectors. WeakReference totodos can resolve cache problem.
constselectItemsByCategory=createSelector(state=>state.items,(state,category)=>category,(items,category)=>items.filter(item.category===category))

Is a more complicated case, ascategory is a string, and we cannot use it for WeakMapping. As wellcategory is not a part of a state (it's a string) and it's "Lifetime" (in Rust terms) is different. In this case we might needpickers, which will convert a POD value into a weak-mappable entry, bound to the given variable for a proper garbage collection.

constselectItemsByCategory=createSelector(state=>state.items,(state,category)=>getPicker(/*from*/items,/*what*/category),(items,category)=>items.filter(item.category===category.value))// wheregetPicker=(source:any,value:any)=>{cosntmap=getOrCreateWeakMapEntry(source);// pseudo codereturnmap.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

You must be logged in to vote
0 replies
Comment options

Hi,@markerikson !
I created and tested a Flipper plugin for Reselect Debugging (it will help to debug Reselect in React Native Apps).
Could you please add into your Comparison table. I took Reselect tools (building dependency graphs) as a basis, but expanded the functionality with open issues in this library.

https://github.com/vlanemcev/reselect-debugger-flipper

Here`s the PR to add this debugger into Reselect README:
#503

Thanks!
I would also like to take this opportunity to say thanks for you hard work on all Redux projects !! 💪💪💪

You must be logged in to vote
0 replies
Comment options

markerikson
Oct 16, 2021
Maintainer Author

Reselect 4.1 is now available, and addresses most of the concerns listed here. It adds a configurable cache size tocreateSelector, 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

You must be logged in to vote
0 replies
Comment options

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'suseSelector tocreateSelector.

I'd like to see reselect own theuseSelector hook and have redux toolkit re-export it as a convienence. Additionally, I'd like to see reselect take ownership of the hook for thecombiners pattern as described bytheKashey.

I'm going to make three assumptions here

  1. It's good thatuseSelector is owned by Reselect
  2. We'll have areselect/react which contains these hooks
  3. Redux Toolkit will expose these hooks in a future release

That said, the following code is for thecombiners pattern, calleduseDerivedSelector.

LikeuseSelector,useDerivedSelector is for accessing state using reselect. UnlikeuseSelector, the derived selector hook only takes other primitive selectors and supports a list of dependencies. Without this hook, developers need to usecreateSelector and take into consideration all of the internal state of reselect.

Hook Example

import{useState}from"react";import{useDerivedSelector}from"redux-toolkit";// from "reselect/react"constMyComponent=()=>{const[items,setItems]=useState();constderivedValue=useDerivedSelector(selectFoo,(foo)=>{// combine foo and items here},[items]);}

Consequences

Hooks 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 thatuseSelector is designed for pure derivation. This is stated in the RTK docs, but byb offering a non-pure alternative, it helps encourage the correct usage of theuseSelector hook.

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 theinitialState, similar to how.actions is created.

Sample Hook

// hypothetical hook code, written quickly to show the idea in practiceconstuseDerivedSelector=(...args)=>{constdeps=Array.isArray(args[args.length-1]) ?args.pop() :[];constcombiner=useRef(args.pop());constresult=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 complexcreateSelector patterns. However, it does offer a clear path of increasing complexity fromuseSelector to the newuseDerivedSelector tocreateSelector when the derivation is no longer straightforward.

You must be logged in to vote
3 replies
@markerikson
Comment options

markeriksonOct 17, 2021
Maintainer Author

Hey, thanks for the comment. I think there's a few misunderstandings here - let me see if I can clarify things.

First, it's important to understand that React-Redux, Reselect, and Redux Toolkit are three entirely different libraries with different responsibilities:

  • React-Redux is the bindings layer between a Redux store and React components, and provides theuseSelector anduseDispatch hooks.useSelector acceptsany selector function, regardless of how it's implemented - a plain function, memoized with Reselect, or written using something else like Ramda.
  • Redux Toolkit is primarily a UI-agnostic wrapper around the Redux core, and includes additional APIs likecreateSlice andcreateAsyncThunk. As of RTK 1.6, it now also includes the RTK Query data caching API. RTKQ in turn has both a UI-agnostic core set of functionality,and a React-specific layer that can generate hooks. That extra React layer requires use of React-Redux to supplyuseSelector/useDispatch as part of the query hooks.
  • Reselect is a small UI-agnostic vanilla JS lib to generate memoized selector functions. While it's advertised as being "for use with Redux", it's not even Redux-specific - you could use it anywhere, including with React or a pure vanilla JS app.

Because of that, it doesn't make sense conceptually to talk about "Reselect owninguseSelector" - that's not possible from either a lib API perspective or a division-of-responsibilities perspective.

Also, the phrase "the majority of hooks are in Redux Toolkit" isn't correct - RTK doesn't haveany hooks except for the RTKQ query hooks.

Wehave vaguely tossed around the idea of adding additional React-specific functionality to RTK at some point, and now that we have the RTKQ query hooks as an additional entry point, we at least have a conceptual pattern that would let us implement that while keeping the bulk of the package UI-agnostic for use outside of React.

As for theuseDerivedSelector suggestion specifically, I'm not quite sure what the specific intent is there. It kind of looks like an equivalent ofcreateSelector but written purely using React's built-in hooks. I could at least imagine some kind of hook that made it easier to create unique selector instances per component or something, but that would be different than what I think you're describing there.

@thecodedrift
Comment options

Yep. I think my mental model of "what goes where & who's responsible for that API" was totally sideways. :) I'll trace back through my notes relearning RTK and see where I made the jump that the hooks came from RTK and not the react-redux layer. If I do find something off in docs, I'll go open a PR over there.

The motivation behind wanting something likeuseDerivedSelector came from there being a large gap betweenuseSelector & needingcreateSelector in a react app. It shows up most commonly when using parameterized selectors and trying to maintain code that utilizes other react hooks.

constselectItems=(s)=>s.items;constMyComponent=()=>{const[filter,setFilter]=useState();constitems=useSelector(selectItems);constfilteredItems="?";}

The docs I referenced when encountering this problem (and my thoughts as I looked at how to solve this):

The thing is, I'm not entirely sure that this is on reselect as a library. What reselect is doing makes perfect sense, but our component has changed in a way that became difficult to follow:

constselectItems=(s)=>s.items;constmakeSelectFilteredItems=()=>createSelector(selectItems,// same as our useSelector from before(_,filter)=>filter,// https://github.com/reduxjs/reselect#api a "props" argument, I think?(items,filter)=>// complex filtering of items goes here// but now it's also decoupled from the component)constMyComponent=()=>{const[filter,setFilter]=useState();constselectFilteredItems=useMemo(makeSelectFilteredItems,[]);constfilteredItems=useSelector((state)=>selectFilteredItems(state,filter));}

We memoize the factory method, then use ouruseSelector hook, but pass in a custom selector that is contingent on the current redux state and the localized filter state. This feels like a long winded way to end up at

constselectItems=(s)=>s.items;constMyComponent=()=>{const[filter,setFilter]=useState();constitems=useSelector(selectItems);constfilteredItems=useMemo(()=>{// complex filtering of items goes here},[filter,items]);}

And again, coming back to the earlier point, I'm not actually sure this is a reselect API issue. It's just thatcreateSelector is all-powerful and there's not really a simpler way to express "this state, plus these external values".

@markerikson
Comment options

markeriksonOct 17, 2021
Maintainer Author

Yeah, there's some scattered references to selectors throughout the React-Redux and RTK docs. That said, I just recently wrote a newDeriving Data with Selectors page in the Redux docs that's intended to consolidate and explain a lot of the usage patterns - I'd suggest taking the time to read through that.

I definitely agree that themakeSomeUniqueSelectorInstance pattern is annoying... and that's actually one of the primary reasons why I opened up this entire discussion issue in the first place :) The core problem is that Reselect's default cache size is only 1, and if you reuse the same selector instance in multiple components, you end up always blowing the cache because every component passes in different arguments. Hence my questions up top about "how can we rework the API to make this easier to use?"

The other issue you're seeing is thatuseSelector by definitionalways calls its given selector function withone argument: the current Reduxstate.useSelector has no way of knowing youmight want to pass in other arguments. That's why the shown pattern isuseSelector(state => selectSomeThing(state, otherArgumentsHere)), because you need to capture those other args from the surrounding environment and pass them through.

Ultimately, yes, there is a lot of overlap between trying to memoize filtered results via acreateSelector memoized selector, and doing it withuseMemo. The names are similar because they do a similar job.createSelector is more flexible overall, both because Reselect offers more customization of selector behavior and configuration, and because it can also be used entirely independently of React.

Comment options

markerikson
Jan 18, 2023
Maintainer Author

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 fordefaultMemoize to enable cache sizes > 1 and reusing shallow-equal results.

We're starting work on Redux Toolkit 2.0, and that will likely mean bumping major versions forredux andredux-thunk. That probably also means bumping Reselect to v5, even if only to change the package exports configuration.

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:

  • What are the biggest pain points with using selectors today?
  • What would an ideal selector API look like?
  • What changes would you want to see made to Reselect?
You must be logged in to vote
5 replies
@theKashey
Comment options

  • What are the biggest pain points with using selectors today?

Not knowing when it works, when it don't, and how to fix it

  • What would an ideal selector API look like?

The current API is quite declarative and simple - you are just connecting the dots and that is all you should be thinking about.

  • What changes would you want to see made to Reselect?

There are options:

  1. make Reselect more strict and punch you every time something is not working properly.
  2. make Reselectmagically disappearing and swallow your mistake driving a better outcome automatically.

The first option is our immutable past, the second is our future already possible in other (👋@dai-shi ) solutions.

@guillaumebrunerie
Comment options

Hey!

First of all, thank you for all your work on Redux, it really is a great library! I have been thinking a lot about how to design selectors in a scalable way, and I am currently using my own custom selector library as I found Reselect (and any other selector library out there) rather inadequate for my needs, so I will describe here the latest iteration of the API I am using to show some of the issues I faced, in the hope that it can inspire a future version of Reselect.

TL;DR

Sorry for the very long post, here is a short list of the key takeaways, described in more details below.

  • Parameterized selectors should be currified functions instead of being functions with additional arguments. Using functions with additional arguments is the main design decision of Reselect I completely disagree with.
  • Caching is tricky, but maybe there is a way to tie it to Redux' global life cycle, cleaning up unused selectors automatically
  • Caching should be optional, as the same API can be used for reducing boilerplate in selectors that have no reason to be cached
  • Reselect's optionresultEqualityCheck is great (but I think that usingequalityCheck just means that the input selectors are broken)
  • The "output selector" is a quite different kind of function compared to regular selectors, so I don’t think the term "selector" should be used for it
  • proxy-memoize was way too slow when I tried it, so I prefer a more explicit Reselect-like API
  • Calling all selectors twice in development to ensure they do not return new references would be great
  • Some more debugging tools for selectors, for instance concerning how many times a given selector is called or how long it takes, would be great

Context

I have been working for the last two years on an internal tool at my current company. It's a graphical editor for creating complex animations (think something like Figma but for animations, with a UI heavily inspired by After Effects), and it's entirely implemented as a React + Redux web app. I have a very complex state structure, as in a given project you have many animations, each animation has many layers, each layer has many keyframes, and so on, and there is a lot of different places the data needs to be queried by components. The state is partially normalized, in the sense that I use the{allIds, byId} pattern everywhere possible (with my custom version ofcreateEntityAdapter), but I also do use nesting: each animation contains its own layers, each layer contains its own keyframes, etc. In particular, to refer to a particular keyframe you need to give theanimationId, thelayerId, and thekeyframeId. Performance is very much a concern as in many places you can click and drag on stuff, which dispatches actions for everymousemove event, and it causes all selectors to rerun every time.

The main issue I faced was how to design parameterized selectors (i.e. selector with arguments) in a way that makes them easy to define, easy to use, and performant. The main function in my library is calledcombineSelectors and is similar to Reselect'screateSelector. It takes as input several parameterized selectors, one combining function, and creates a resulting parameterized selector. Let me now get into more details about exactly what I mean, as well as clearing up some terminology.

Simple selectors

A (simple)selector is any pure function that can be used as an argument to theuseSelector hook. In particular it has exactly one argument, the whole state, and returns some data.

In addition to being pure, it is very important for selectors not to return new references needlessly, likefilter ormap do, otherwise the components using that selector will rerender at every single state change which can impact performance dramatically. I know that theuseSelector hook can be passed an equality comparison function as its second argument, but in my opinion this is the wrong place to put this equality test: it really is the responsibility of the selector to not return new references, not the responsibility of the component to know which selectors require shallow equality tests and which do not. The newly addedresultEqualityCheck option in Reselect is a much better solution.

In my library, I also added a debugging feature that calls all selectors twice (in development mode) and logs an error message if the two results are not referentially equal, because that means something is wrong with the selector. I found this invaluable in order to find selectors that may be improperly memoized.

Simple selectors are fine for simple global state, but in my case the vast majority of selectors need to select data corresponding to a particular animation, or to a particular layer in a particular animation, and to handle that we need parameterized selectors.

Parameterized selectors

Aparameterized selector is a currified function that takes some arguments and returns a simple selector, for instance:

selectAnimationById:({animationId:string})=>(state:State)=>Animation

Note that this differs from Reselect's notion of parameterized selectors, which is a function with several arguments in addition to the state:

selectAnimationById2:(state:State,animationId:string)=>Animation

I must say I really don't understand the point of such selectors with several arguments, because they cannot easily be used inuseSelector (except by writing anonymous functions everywhere, but who wants to do that?). Compare:

constanimation=useSelector(selectAnimationById({animationId}));constanimation=useSelector(state=>selectAnimationById2(state,animationId));

I always want to use the first version, so that's what I tailored my library towards. I don't support selectors with several arguments, and I don't even call them selectors.

Combining functions

The "output selector" of Reselect is what I call acombining function. I don't think the word "selector" should be used here at all because it is a completely different kind of function:

  • It takes several arguments, none of them being the whole state, whereas selectors only take one argument which is the whole state.
  • Unlike selectors, it is perfectly fine for the combining function to return new references, that's actually exactly where you are supposed to do that.

So I think it makes sense to clearly distinguish between (parameterized) selectors and combining functions because they correspond to different concepts.

Another related point is that I don’t really see the point of Reselect'sequalityCheck option. The input selectors should be proper selectors themselves and should therefore never need anything else than a reference equality check. If an input selector returns new references, then it should be fixed to not do that, rather than requiring every derived selector to useequalityCheck.

Caching

If the combining function is computationally expensive/returns new references, then caching can/must be used. Like in Reselect, caching means that we first run all input selectors, and if it turns out that we get the exact same values than last time, then we do not rerun the combining function, but instead simply return whatever it returned last time. A very important thing to look out for (which is what re-reselect was designed to fix), is that calling the same selector but with different arguments should not invalidate the cache, otherwise it completely destroys all memoization as soon as you have two different uses of the same selector at the same time:

// No memoization at all with Reselect's default behavior!conststuff=useSelector(selectComputationallyExpensiveStuff(animationId));constotherStuff=useSelector(selectComputationallyExpensiveStuff(otherAnimationId));

Figuring out how to properly cache thing is pretty tricky, though.

The simplest solution is to have a infinite cache, but it can of course create memory leaks. But a fixed cache size (as in Reselect'scacheSize option) doesn’t make a lot more sense to me either, as

  • it requires the programmer to choose a somewhat arbitrary number
  • if it turns out that this number is too small, it completely destroys all memoization and replaces it by useless bookkeeping: if you have a cache size of 10 but you use 11 different instances of a given parameterized selector, then at every render eachuseSelector will remove from the cache the value that is about to be used by the next selector...

Ideally I think that there should be an unlimited cache but which gets cleaned up after each render, so that we only cache instances of selectors that are currently in use. I think this is where 99.9% of the benefits of caching come from: to optimize successive renders and making sure that when we change one part of the app, the rest of the app that is untouched doesn’t rerender/runs computationally expensive functions. So every time an action is dispatched and Redux has run all the selectors again, it should remove from the cache all cached selectors that did not rerun in this cycle. I haven’t implemented that in my library yet, but I think that would be the best solution.

In addition, re-reselect allows you to provide a custom function to compute a cache key based on the arguments. In my case, I feel likeJSON.stringify on the arguments is enough, but I guess it might be occasionally useful to have a custom cache key (although it still feels a bit too low-level to me).

Globalizing selectors and reducing boilerplate

One of the main problem I wanted to solve was actually reducing boilerplate when globalizing selectors. For instance a keyframe has a timestamp, so there should be aselectKeyframeTimestamp parameterized selector. In "raw Redux" you could write it like this:

constselectKeyframeTimestamp=(props:{animationId:string,layerId:string,keyframeId:string})=>(state:State)=>state.animations.byId[props.animationId].layers.byId[props.layerId].keyframes.byId[props.keyframeId].timestamp

but this is clearly not sustainable, especially when you need many such selectors. Instead I can now write it as follows, whereselectKeyframeByIds is defined once somewhere else.

// selectKeyframeByIds has type// ({animationId: string, layerId: string, keyframeId: string}) => (state: State) => KeyframeconstselectKeyframeTimestamp=combineSelectors([selectKeyframeByIds],keyframe=>keyframe.timestamp);// with type// ({animationId: string, layerId: string, keyframeId: string}) => (state: State) => number// Usage in a componentconsttimestamp=useSelector(selectKeyframeTimestamp({animationId, layerId, keyframeId}));

which is a lot better. The thing to note in particular is that I am not usingcombineSelectors here for its caching capabilities, but only for reducing boilerplate. Caching is not desirable here as it would just increase memory usage for no good reason. Given that most of my selectors are of this form, I actually have caching off by default, and I turn it on only for the selectors that do need it.

The API

Finally, here is how my API looks like on a general example.

Given several parameterized selectors:

selector1:(props:Arguments1)=>(state:State)=>Result1selector2:(props:Arguments2)=>(state:State)=>Result2selector3:(props:Arguments3)=>(state:State)=>Result3

and a combining function

combiner:(value1:Result1,value2:Result2,value3:Result3)=>Result

it produces a new parameterized selector

combineSelectors([selector1,selector2,selector3],combiner):(props:Arguments1&Arguments2&Arguments3)=>(state:State)=>Result

which callsselector1,selector2 andselector3 onprops andstate, and then callscombiner on the three results. There is support for optional caching and result equality check via a third argument tocombineSelectors. I currently only support (and need) parameterized selectors with one argument, and the type of the argument of the resulting selector is the intersection of all the types of the arguments of the input selectors. This allows the input selectors to share arguments, which can be very useful in practice.

For instance let's say I want the timestamp of a keyframe but expressed in frames (which requires knowing the fps of the animation), I can write it like this:

// Assume we have// selectAnimationById: ({animationId: string}) => (state: State) => Animation// selectKeyframeByIds: ({animationId: string, layerId: string, keyframeId: string}) => (state: State) => KeyframeconstselectAnimationFps=combineSelectors([selectAnimationById],(animation)=>animation.fps,);// of type ({animationId: string}) => (state: State) => numberconstselectKeyframeTimestampInFrames=combineSelectors([selectKeyframeByIds,selectAnimationFps],(keyframe,fps)=>keyframe.timestamp*fps,);// of type ({animationId: string, layerId: string, keyframeId: string}) => (state: State) => number

and then I can simply use it as follows

consttimestampInFrames=useSelector(selectKeyframeTimestampInFrames({animationId, layerId, keyframeId}));

It will call bothselectKeyframeByIds andselectAnimationFps with the argument{animationId, layerId, keyframeId} and the state, which will return the keyframe we're interested in and the fps of the corresponding animation, which are then passed to the combining function which computes the final result.

The other API I am using is the following function, which allows you to add arguments to a selector:

constselectArgument=<Kextendsstring>(key: K) =><T>(props: Record<K,T>) =>(_:State)=>props[key];

It is a function returning a parameterized selector that ignores the state and simply returns one of the parameters. It can be used to add additional arguments to a selector, for instance if you want the fps to be provided as an argument to the selector rather than taking it from the animation:

constselectKeyframeTimestampInFrames=combineSelectors([selectKeyframeByIds,selectArgument("customFps")<number>],(keyframe,fps)=>keyframe.timestamp*fps,);// of type ({animationId: string, layerId: string, keyframeId: string, customFps: number}) => (state: State) => number

Additional thoughts

I tried to useproxy-memoize at some point (maybe one year ago or so), but performance was pretty bad and it made my app essentially unusable, so I quickly moved away from it. It is fully possible that I was not using it properly or that it improved since then, but on the other hand selectors run way more often than reducers, so it might be an inherent problem to proxy-based selectors. I do use Immer for reducers, but it is acceptable there as there is typically at most one reducer running per mousemove event, as opposed to several hundreds of selectors.

Another thing I implemented in my library is a debugging tool that counts how often selectors are called and how much time they take to run, to find computationally expensive selectors. I have found that this gives great insights and allows one to know whether some selectors need to be optimized.

@markerikson
Comment options

markeriksonMar 28, 2023
Maintainer Author

@guillaumebrunerie thank youvery much for taking the time to write up this extensive comment! I genuinely appreciate it.

Unfortunately my own brain is kinda fried and overloaded atm (a combination of not quite enough sleep the last couple days, and juggling too many other tasks at work and on the Redux side atm).

I've skimmed your comment, and there's clearly a lot of great info here. Iwill come back to this later and give it the deeper read and better response that it deserves, but it will probably be some time before I'm ready to turn my attention back to working on Reselect.

My couple immediate thoughts I can toss out are:

  • Yes, wedo specifically show theuseSelector(state => selectAnimationById2(state, animationId)) approach in the docs as the typical way to use multi-param selectors, because it's clearer what's going on as opposed to a curried selector being generated
  • I'd love to see more details on the API and implementation of yourcombineSelectors lib if you get a chance
  • Agreed that fixed-size caches are of debatable use. Ihave seen an interesting technique inReact's experimentalcache() API implementation that usesWeakMaps to store cache data as a tree based on the arguments - something that's on my list to go investigate further.
@theKashey
Comment options

So every time an action is dispatched and Redux has run all the selectors again, it should remove from the cache all cached selectors that did not rerun in this cycle.

Brilliant idea - automatic garbage collection for the cache. It is worth further investigation.
I was looking for a similar solution atmemoize-state (almost 5 years ago) and found it not to work with "magic memorization". In my case a small update to the state will preserve the majority of selectors and not trigger deeper computations, marking them as "not used".
Look like a bit more direct solution will be not affected by such "zerba tainting". Very interesting 👍

I have seen an interesting technique inReact's experimental cache() API implementation that uses WeakMaps to store cache data as a tree based on the arguments - something that's on my list to go investigate further.

It's been8 4 years -https://github.com/theKashey/useReselect

@guillaumebrunerie
Comment options

@markerikson I have now made a codesandbox showing my implementation, seehere. I've plugged it into some default Redux template to check that it works, but this is just an example application, way simpler than my real project.

  • The main file to look at issrc/store/combineSelectors.ts (containing the implementation)
  • You can also look atsrc/store/selectors.ts (containing some simple selector definitions) andsrc/component/Todo.tsx andsrc/components/TodoList.tsx (showing the usage of those selectors in components). I could try to come up with more complex examples if desired.
  • There are some Typescript/ESLint parsing errors, but this seems to be because codesandbox uses an old version of Typescript. It works fine for me otherwise.
  • Similarly, all theas const inselectors.ts can be instead replaced by a singleconst modifier incombineSelectors, but that also requires a more recent version of Typescript.
  • I need to import theAppState type incombineSelectors.ts, which is of course suboptimal, but so far I haven’t managed to make it infer it automatically (I also haven’t tried very hard).
  • I removed the debugging stuff because I couldn’t make it work with codesandbox: I need to extract the name of the selectors, and I do that in my real project in a very hacky way by fetching thebundle.js file created by Webpack and then parsing a(new Error()).stacktrace to extract the exact piece of code where the selector is defined, and then some more fiddling around with promises as it is asynchronous.
  • The empty object in for instanceuseSelector(selectIncompleteTodoMessages({})); is just a flaw in my Typescript typings which makes every parameterized selectorrequire one argument, even if it is not used.

When it comes to anonymous functions likestate => […] being clearer than curried functions, I really have the opposite opinion:

  • From the point of view of the consumer of the selector (the person writing a component), the fact that a selector is a simple function taking the state as an argument is an implementation detail.
  • In other words, it is completely irrelevant what a selectoris, as long as you can feed it touseSelector and get in return the data you were promised.
  • In addition, we could imagine the selectors created bycombineSelectors to be more than simple functions (for instance functions with some debugging or testing information attached to them, like therecomputations field in Reselect), and thenuseSelector could make use of that information when available, but this wouldn’t work at all when only passing anonymous functions touseSelector.
  • Finally, (as a response to one of your comments below) note that that every call tocombineSelectors sets up its own local cache and then caches the (resulting) selector functions themselves, based on the arguments. This is necessary, as you point out, otherwise there is no way memoization could work. So when calling a selector created withcombineSelectors, it only creates a new function reference if that selector has never been called before with those arguments, otherwise it returns the cached selector. Having curried functions doesn’t make it especially more difficult to do, it’s just a matter of declaring the right variables in the right place.
Comment options

Hey Mark,

Thanks for looking into this. As I mentionedon twitter, it would be nice to have a version ofcreateSelector() that, in an intuitive way, creates a function that takes parameter(s) and returns a selector (which would be used likeuseSelector(selectUser(id))). I imagine it looking similar to createAsyncThunk. For example:

(copied fromrtk docs)

interfaceMyData{// ...}interfaceMyKnownError{errorMessage:string// ...}interfaceUserAttributes{id:stringfirst_name:stringlast_name:stringemail:string}constupdateUser=createAsyncThunk<// Return type of the payload creatorMyData,// First argument to the payload creatorUserAttributes,// Types for ThunkAPI{extra:{jwt:string}rejectValue:MyKnownError}>('users/update',async(user,thunkApi)=>{const{ id, ...userData}=userconstresponse=awaitfetch(`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 handlingreturnthunkApi.rejectWithValue((awaitresponse.json())asMyKnownError)}return(awaitresponse.json())asMyData});

I would love to have something similar withcreateSelector(). The extra argument would only be passed to the last selector function. So if I had more than one selector, it would work like this:

constselectUserById=createSelectorWithParameters<// type of root stateRootState,// Type of the first argument to the selector creator functionnumber>(// 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.jsxfunctionMyComponent(){constuser=useSelector(selectUserById(3));// ...}
You must be logged in to vote
7 replies
@triptu
Comment options

There is another way to solve this which maintains the memoization capabilities,useSelector(selectUserById, id). I've started to use this in a recent app and was wondering if there is a downside with this approach as well. This is a custom store on top of Zustand with overrides their subscribe method to handle the above format.

@guillaumebrunerie
Comment options

@triptu Do you mean that you have a customuseSelector hook that basically does:

import{useSelectorasuseSelectorOriginal}from"react-redux";exportconstuseSelector=(selector,props)=>{returnuseSelectorOriginal(state=>selector(state,props));};

I like this syntax, and I've been thinking about introducing it to my own projects as well, although it's kinda weird to use the same name (useSelector) with a different API, it might be confusing to other people.

@triptu
Comment options

@guillaumebrunerie yes very similar, typed like this with typescript to ensure the IDE autocompletion works fine.

exporttypeStateSelector<T,R,Argsextendsany[]>=(state:T,  ...args:Args)=>R;exportconstuseStore=<R,Argsextendsany[]>(selector:StateSelector<T,R,Args>,  ...args:Args):R=>{returnuseSelector((state)=>selector(state, ...args));};// usageconstrooms=useStore(selectRooms);constroom=useStore(selectRoomById,roomId);constuserConfig=useStore(selectRoomUserConfig,roomId,userId);

same name with a different API

I useuseStore for the name usually with the app name in it as well(e.g.useGithubStore). In my case the store part is abstracted as its own small wrapper and doesn't export the original hook to keep things simple.

I have a lot of selectors which take arguments and really like having them defined at one common place. This especially becomes helpful when the selectors are doing more than just selecting one field. The DX is good.

@guillaumebrunerie
Comment options

@triptu Actually, I just realized that separating the selector function from the arguments makes it very easy to implement a custom hook for the"selector factory" pattern, which ... should solve essentially all cache problems?

exportconstuseParameterizedSelector=(selectorFactory, ...args)=>{constselector=useMemo(selectorFactory,[]);returnuseSelector(state=>selector(state, ...args));};

With this, each use ofuseParameterizedSelector creates a memoized selector with its own cache of size 1, which is perfect.
The selector simply needs to be defined as a selector factory, that is

constselectorFactory=()=>createSelector(...);

instead of

constselector=createSelector(...);

Have you tried something like that?

@triptu
Comment options

Cool idea. The main issue I see is that there will be duplicate memoization in contrast to using maxSize with the defaultMemoize function. Every place which needs sayselectUserById will create its own instance instead of one shared instance. This is probably fine for most applications though but might increase memory usage at scale.

Comment options

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 haveconst selectSelf = (state: RootState) => state.user

Rather than this:

constselectUserAddress=createSelector((state:RootState)=>state.user,(user:UserState)=>user.address);

We would have this:

constselectUserAddress=createSelector((user:UserState)=>user.address);
You must be logged in to vote
1 reply
@markerikson
Comment options

markeriksonApr 7, 2023
Maintainer Author

We do have some plans in RTK 2.0 to try adding aselectors field inside ofcreateSlice:

There's a WIP PR here - take a look and give feedback?

reduxjs/redux-toolkit#3297

Comment options

Posting about performance problems in Reselect in response tothis.

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 Stage

The 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 Problem

We can't buffer updates for too long (about 2-3 seconds was the hard limit for most data streams):

  • Too many simultaneous updates puts too much strain on Redux itself. The store's performance was actually pretty admirable, but at a (very high) point, the thread-blocking can become visible momentarily. 2-3 seconds worth of buffered updates was well under that point for us.
  • Some data can't be buffered too long for UX reasons - e.g. data streamed in response to user input.
  • In our tables, lots of cells flash at once when you do flush after a long wait, which feels glitchy for our particular app.

But if we flush updates too often, too many calculations have to run too often:

  • Some selector graphs are very large - many fields and operations get their own selector where possible to prevent reruns further down the chain if at all possible. This means lots of diffing, some of it via lodash'sisEqual1.
  • Many operations involve sorting.
  • Immutable toJS/toArray calls have to run before some data gets to React elements. We were always walking the fine line between Immutable's overhead being worth it (for its state update speed) and not (for its slow transformations).
  • Rerenders and DOM updates themselves have to take some mandatory time too.

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 Interlude

Before 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 Fix

In 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 outhere.

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 State

With 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 subscriptionsreduxStore.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/Throttling

Atoms 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 treeonly 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:constentitiesAtom=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'],},}))constgetFruits=({ get})=>get(entitiesAtom).fruitsconstgetApples=({ select})=>select(getFruits).applesconstgetLemons=({ select})=>select(getFruits).lemonsconstgetLimes=({ select})=>select(getFruits).limesconstgetOranges=({ select})=>select(getFruits).orangesconstgetCitrusFruits=({ select})=>[  ...select(getLemons),  ...select(getLimes),  ...select(getOranges),]constgetSortedCitrus=({ 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, ImmutabletoArray(), etc.

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):constcitrusFruitsAtom=ion('citrusFruits',({ select})=>{conststore=injectStore()constselectors=[getLemons,getLimes,getOranges]// subscribe to updates in upstream selectors, but discard results hereselectors.forEach(selector=>select(selector))// a custom injector that hides implementation details. See below if curiousinjectThrottle(()=>{store.setState(selectors.flatMap(selector=>select(selector)))})returnstore})
(ignore me, I'm just some implementation details for the above example)

Here's an example of howinjectThrottle() could be implemented:

constinjectThrottle=updateState=>{// set initial state on first runif(!injectWhy().length)updateState()// injectors are like hooks for atoms. This works just like `useMemo` ...constsubject=injectMemo(()=>newSubject(),[])subject.next('update happened!')// ... and this works just like `useEffect`:injectEffect(()=>{// use RxJS to throttle updates, only updating every 2 secondsconstsubscription=subject.pipe(throttleTime(2000)).subscribe(updateState)return()=>subscription.unsubscribe()},[])}

Full codesandbox demonstrating thishere.

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 Streams

This 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 Finale

So 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'smemoizeOptions wasn't a thing yet. We had some custom selector creators for managing caching, result comparison, and feeding the previous value to the selector function.

You must be logged in to vote
3 replies
@markerikson
Comment options

markeriksonApr 29, 2023
Maintainer Author

Thank you for the writeup! I have to head out in a minute, so I'll try to come back later and give it some more thought. My first reaction is that both React and Redux work best in the "80%" use case, where there's decent amounts of data being updated reasonably frequently, but when you start hitting these "real-time/thousands of updates per second" use cases that's not really what they're meant to handle. That's also true for Reselect.

In general, I'd love to come up withsome kind of mechanisms that help better optimize Redux updates and Reselect selectors, but I'm not sure thereare alternate approaches without fundamentally changing both libraries.

@bowheart
Comment options

Yep, so similar conclusion. The 3 fixes I gave represent 3 different categories of problems:

  1. Problems the data model can improve (e.g. atomic vs singleton state)
  2. Problems the selection model can improve (e.g. offering more control over memoization details and evaluation frequency across the selector graph)
  3. Problems that break down the reactive paradigm itself and can't reasonably be improved in React or React-based models (our lib falls short here too and can only help by offering an escape hatch and resources/guides to help people do it correctly).

As far as whether any of these are actionable for Reselect, I'm in the same boat: I don't think so. At least not for the specific problems we ran into. The current paradigm is good enough for most things (way more than 80% 😄) and any changes would require massive overhauls. Not worth it.

What I was more intending to communicate is that React itselfwas fast enough for us in every case but one. However, React's overheadcombined with Reselect's overhead was too much for us in lots of other cases too. Reducing our data layer overhead enabled React for us in all those other cases.

React's flavor of reactivity has amazing Dev X and I'm glad we didn't shun it just because our app type technically lies outside React's intended use case. IMO it's way better to use React for everything possible and bump down to imperative, brittle models only exactly where you need them than to use a view layer with worse Dev X for everything. React gives all the escape hatches necessary for this.

Anyway, I hope this was helpful or at least alittle interesting.

@guillaumebrunerie
Comment options

I wonder if you took a look at SolidJS? It's an alternative to React with pretty similar syntax but a quite different model of reactivity, and it's been praised for being a lot more performant. The reactivity model is based onsignals (single reactive values whose life cycle is not tied to components, similar to your atoms I think) andeffects (side effects that rerun whenever the signals it depends on change). You can create graphs of signals that depend on other signals (usingcreateMemo), and you can debounce/throttle parts of the graph to make sure some signals do not change too often (using for instancecreateDebouncedMemo fromsolid-primitives). So everytime a signal changes, the signals that depend on it rerun, and so on until reaching the individual DOM nodes that use a signal that just changed, which are then modified atomically. There is not "rerun every single selector at every change" phase like in Redux, nor "rerun the whole component code including every child at every change" like in React. It's really trying to do the minimal amount of work to update the UI.

I'd suggest watching the talk "The world beyond components" by Ryan Carniato, it's more about how SolidJS is a replacement for React rather than Redux and Reselect, but it's really an eye opening talk.

Comment options

A little offtopic, butwhat problem we solve? It's some sort of mnemonic to clarify the problem.

And we try to solve many different problems:

  • proxy looking at the usage ➡️ not to bother selectors when unrelated slices changes
  • different mechanics to aid caching in general using quite different methods
  • technically there is nothing else. Performance and stability, nothing else.

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.
For example, instead ofweakmap.get(object), with theobject pointing at the different entity next moment, one can doweakmap.get(object[cacheSymbol]) and information stored behind magic symbol will be preserved during store updates.

This is less about what wecould do, more about the idea of looking for a problem not forreselect as a library in isolation, but forreselect as a part ofredux.

  • for example, the first moment about not reacting to store updates might be a redux's problem and the solution for it should be there.
  • as another example, might be "stability" could be partially solved in dev time (call selector twice, results should be equal), pointing the developer on a problem. In this case, there will be fewer moments to be managed by reselect.
  • in both cases we not onlyreduce scope, we change it.
You must be logged in to vote
2 replies
@markerikson
Comment options

markeriksonApr 30, 2023
Maintainer Author

Note that I did add user-configurable cache sizes todefaultMemoize as of Reselect 4.1.

I'm also playing around with a couple new memoizers over in#605 - an "autotrack" memoizer based on ideas from Ember's Glimmer engine, and a "weakmap" memoizer based on React's internalcache() API. Both still early POCs.

@theKashey
Comment options

I am keeping an eye on your activity, and it looks very promising 👍.
I just wanted to remind that not every problem need a solution in isolation, only as a part of a bigger whole. This probably would not affect what you are doing, as you working on a level above Reselect. But quite related to the discussion above, there another "level above" has been discussed as a potential solution.
This is why the question - is the goal to solve the problem for reselect, or reselect with redux (and react?). Experiments withcache() sounds that we are on the same page.

Comment options

markerikson
Oct 28, 2023
Maintainer Author

General update for folks:

Perreduxjs/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 areprimarily focused onmodernizing our published ESM/CJS compatibility, but have some tweaks around removing deprecated APIs, improving TS types, etc.

For Reselect 5.0 specifically: I'm stillnot making any massive changes tocreateSelector's design or public API. The two meaningful changes are:

Right now all I want is to get the RTK 2.0 release wavedone so that folks can use the updates we've got, without major breaking changes. We've already deferred any RTK Query feature changes toafter 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!

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
Ideas
Labels
None yet
15 participants
@markerikson@thecodedrift@dai-shi@theKashey@nathggns@guillaumebrunerie@DieTapete@Soundvessel@adamerose@bowheart@ghmcadams@triptu@vlanemcev@artalar@eXamadeus
Converted from issue

This discussion was converted from issue #490 on February 16, 2021 16:47.


[8]ページ先頭

©2009-2025 Movatter.jp