-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
options.arePropsEqual for custom prop equality comparison #183
Conversation
What's the value proposition here? What comparison function besides |
Oh okay that makes sense to me. So this is basically React Redux's version of Could we rename the option to be more precise? The name |
I think we've concluded it's actually not possible to do the per-component memoization without a global LRU cache as @ellbee mentions here, so this would be the next best thing. @acdlite I typically like to use a one layer deeper shallow I've found the amount of time it takes to do an extra shallow compare of a few Arrays is virtually nothing compared to the expense of React going through an update cycle. |
If we're optimizing render cycles, that can be addressed using If we're optimizing selectors / action creator binding, then this option makes sense. |
It's sort of both, but yes if the connected component is receiving props containing Object/Array values, I'd imagine depending on the use case it too could benefit from doing a slightly more sophisticated |
@@ -53,7 +53,7 @@ ReactDOM.render( | |||
|
|||
Connects a React component to a Redux store. | |||
|
|||
It does not modify the component class passed to it. | |||
It does not modify the component class passed to it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was probably your editor being helpful, but the trailing whitespace here was to create a line break in the rendered markdown, and should be put back.
This looks reasonable to me. |
d61d94b
to
d193981
Compare
@tgriesser Appreciate the thoroughness of trying out all these approaches! I wish I had some time right now to look at this and give some thoughts, but something's come up and I'm not going to have any 'github time' at all in the next few days 😞 |
@ellbee no worries / no rush, whenever you get around to it! I've had the same problem with my own projects lately, have no problem working off of a fork. I really appreciate a project which takes longer to figure out the simplest solutions rather than to just merge everything - keep up the great work all! 👍 |
@tgriesser Assuming we can think of an acceptable name for the option I think I like this PR the best; it doesn't add to the top level API and changes to existing code are minimal. Coming at it from the import LRU from 'lru-cache'
function defaultEqualityCheck(a, b) {
return a === b
}
export default function memoizeWithCache(
func,
hashFn,
cacheOptions = 500,
eq = defaultEqualityCheck
) {
let cache = LRU(cacheOptions)
return (...args) => {
const key = hashFn(args)
let result = cache.get(key)
if (result && args.every((arg, i) => eq(arg, result.args[i]))) {
return result.result
}
result = func(...args)
cache.set(key, { result, args })
return result
}
} import memoizeWithCache from './memoizeWithCache'
import { createSelectorCreator } from 'reselect'
import shallowEquals from 'is-equal-shallow'
// take the second argument and use as the hash key
const hashFn = args => args[1]
const createSelectorWithCache = createSelectorCreator(
memoizeWithCache,
hashFn,
500,
shallowEquals
)
// each row in state.table will be rendered with a separate connect
const selector = createSelectorWithCache(
(state, props) => state.table[props.rowId],
// pass the row into the result fn to use as the cache key
(state, props) => props.rowId,
// key is not used in resultFunc, but it is getting picked out by hashFn to use as the cache key
(rowData, key) => {
return {values: rowData}
}
) The |
👍 |
Ah cool, thanks for sharing this does look pretty neat - I'll need to dig in a bit and see how this ends up working in practice. At first glance I still believe it requires you to know too much about the shape of the component props you're connecting to in order to properly use the cache. If a key is required to make guarantees it seems it'd be leaking the selector implementation details into the component. While it probably works well it, my initial impression is that it's the wrong place to be solving the issue of having every connected component instance share a singleton selector. Top level API concerns aside (and I'd contend it's not much of an addition since it's really a higher order |
Yeah, you do have to specify how to pick out the "cache key" from the props, but you already have to know the shape of the props to write the selector. And if you aren't using props in the selector then you don't have the problem in the first place.
I am struggling to think of a practical situation where you would have to add a dedicated "cache key" prop, I think that that an existing prop (or some combination of existing props) should be enough to define the key, although this is probably a failure of imagination on my part.
I don't see a problem with a singleton selector that has a cache so I think our instincts differ on this (although I concede that maintaining a selector library means I am probably a little biased to the selector based solution so I am happy to hear arguments that may persuade me otherwise 😄). One scenario in which the caching selector may be preferable is if there are 10 components with one set of props, and 10 components with another set of props. In this case the selector computation would run twice for every state update rather than the 20 times it would run if selectors are being created for each component.
The thing I am unsure about with #185 is whether it is useful enough outside of this (possibly niche) use-case to warrant an addition to the top level API. It is also a bigger change (in terms of lines of code) than this PR, and in my opinion makes the code a little harder to understand. On the plus side it is probably easier to explain from a documentation point of view. Can a case be made that it is also more general and therefore enables more use-cases than this PR? It seems like it should but I can't think of examples. Is #185 your preference? |
So the thing is, I'm actually creating selectors in a @connect(createStructuredSelector({
value: (state, props) => props.__valueSelector(state),
field: (state, props) => props.__fieldSelector(state)
}))
class SomeComponent extends React.Component {
// ...
}
@pureRender
export default class SomePureComponent extends React.Component {
render() {
return (
<SomeClass
__valSelector={valueSelector(props.valueId)}
__fieldSelector={fieldSelector(props.fieldId)} />
)
}
} I'm sure the LRU cache could be configured to work here, it'd just be a lot uglier. The issue is that when different prop selectors are used against the same structured selector, it invalidates the final structured selector's memoization pretty much every time. I'm using a single decorator which combines @connectSelector({
value: (state, props) => props.__valueSelector(state),
field: (state, props) => props.__fieldSelector(state)
}))
class SomeClass extends React.Component {
// ...
} I think #185 would be my preference, just because it solves the problem more generally in one place, as opposed to going through the ~150 or so Also might be worth getting some benchmarks to make sure that any changes aren't major regressions for common cases... I could take a shot at that in a week or two once I have a bit of extra time. |
Ah, thanks for writing that up. Interesting to see your use case. I'll take a closer look over the weekend when I get some free time. I think benchmarks would be cool if you have the time. |
(I am coming from similar issue in Let me do a recap. In short, all we want is support per-instance memoize function. I think there are few things we could do:
Since @tgriesser suggested we could hold something per-instance by either:
I like #185 "thunk" approach more because "props" approach requires another wrapper or knowledge in parent. "Thunk" approach is cleaner. Consider the following nested selector scenario, In the old days, we write something like this, and this doesn't works well with multiple instances: const quotationSelector = createSelector(
[
state => state.quotations,
(state, props) => props.quotationID
],
(quotations, quotationID) => ({
quotation: quotations[quotationID]
})
);
const welcomePageSelector = createSelector(
[
state => state.profile,
quotationSelector
],
(profile, quotation) => ({ profile, quotation })
);
connect(welcomePageSelector)(React.createClass(...)); This doesn't works very well if there are multiple components on the page and each have different props. The memoize function inside Then in the new days (with "thunk" approach): const quotationSelectorFactory = () => createSelector(
[
state => state.quotations,
(state, props) => props.quotationID
],
(quotations, quotationID) => ({
quotation: quotations[quotationID]
})
);
const welcomePageSelectorFactory = () => {
const quotationSelector = quotationSelectorFactory();
return createSelector(
[
state => state.profile,
quotationSelector
],
(profile, quotation) => ({ profile, quotation })
);
};
connectComponent(welcomePageSelectorFactory)(React.createClass(...)); Note that I am almost good with #185, except that the name Update: In short, I think what we are doing is:
And we are finding ways, either:
|
Yep, @compulim gave a great summary of the value proposition of this API addition, and why I believe #185 is correct solution to proper memoization per component instance without needing to alter the existing
Totally. I am up for changing this, and wanted to see what anyone else thought might be a good name. I initially had
I don't think changing the current |
@tgriesser, thanks so much for your work on this!
|
@oyeanuj, for your question.
I think we should not break existing usages. But I do agree the current implementation is weak. @tgriesser, I think Your code is good, I really hope to see it in the official branch sooner. |
+1 to getting this merged soon! @ellbee? :) @tgriesser What documentation do you think we need in the guide/docs for this? |
@oyeanuj Yeah, sorry for not being responsive on this, been super busy 😦 I am going to make some time in the next couple of days to get to all my loose ends. |
Any updates on this? I've got a TON of uses for per-component memoization, and would love to see this become an option. |
I'm happy to get this in, but it doesn't merge cleanly. |
@tgriesser do you need some help to bring this branch up-to-date? |
@compulim I am happy to clean this up for this merging if this is the PR we want to use but there is also #185.. @gaearon #185 is another proposal for solving this issue, and although the discussion is happening on this PR, #185 seems to be the preferred proposal of the participants in this thread. #185 is not a breaking change, but it does introduce a new "connect factory" function to the API, while this PR only adds an option to Both PRs look fine to me, so I don't have a big preference. |
Does #185 (comment) make any sense? |
Closing in favor of #185 (comment), assuming I'm not mistaken in that it would work. |
Adds new
arePropsEqual
option, defaulting toshallowEqual
if not provided.Docs & test included!