-
Notifications
You must be signed in to change notification settings - Fork 0
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
proof of concept for reselect without connect #9
Open
naw
wants to merge
1
commit into
memoize-functions
Choose a base branch
from
reselect-without-connect
base: memoize-functions
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 strikes me as undesirable because
visibleTodos
is required to be here for for this class to work, but it's not used directly anywhere insidethis
. IfvisibleTodosSelector
was defined in a different file and/or if multiple connected components used that selector, it would become very hard code to follow (and probably maintain):To generalize that- with this approach, looking up the chain of dependent selectors, you go up the chain and eventually the dependencies of a "top level" selector are the props of [every component that uses the selector]. With a selector that's not a top level selector, like
matchingTodosSelector
, you have the dependency that [every component that uses the selector] definesvisibleTodos
as a property, even though it's not used directly, or passed directly to anything inclass App
.Or:
This is in contrast to regular connected reselect, where each selector's dependencies are clearly defined in one place (even if they're not in the same file, they're still in one place, and where they come from if in a different file is explicated at the top of the file in the
import
statement). For the "top-level" selectors, they only have one dependency, the state, and that dependency is super easy to watch change, and we always know where it is.Or:
Basically I think a lot of complexity is added in going from:
to:
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.
I think your points are helping to distinguish a difference between global selectors for denormalizing state that components would want to receive as denormalized vs. computations that a component may want to memoize.
This PR proved you could use
reselect
as the memoizing engine for component-specific calculations, but it's really serving a different purpose within the app than a global selector.Let me explain:
Suppose the redux store has a list of specialties, and we want to transform those into a different data structure for easier consumption by a
<Select> component
Also suppose two different components need the specialties options, say<NewReferralPage>
and<NewServiceRequestPage>
. I think it could make sense to use areselect
selector to denormalize the data globally, and then useconnect
(once for each page component) to pass the denormalized data into each of those two pages as a property likespecialtyOptions
.On the other hand:
Now suppose we add
<Pane>
components to the todo app, with each pane having its ownsearchTerm
andvisibilityFilter
. Each pane should take three properties:todos
,filter
, andsearchTerm
. Each pane needs to combine those properties into the derived data that the pane wants to display. Computing this data in a memoized fashion is important, and this PR shows how that could be done by leveraging thereselect
library, even though it's serving a different purpose than a global selector.In many cases, we may have a component that needs to make calculations, and we may want to memoize those calculations. If there is only one kind of component that needs such computations (for example, a todo pane is the only kind of component that needs to know about
visibleTodos
andmatchingTodos
, there isn't much value in defining a reusable selector that knows how to reach into the component to find the inputs.However, if there are multiple kinds of components that need to perform the same kind of computations on the same kind of inputs, then we could extract the logic for "finding" the inputs within the component into a reusuable module ---- in such cases, we're building an implicit contract for what methods those components need to expose. This is completely analogous to our
Personable
module in Ruby, where the implicit contract is that the class must expose afirst_name
and alast_name
method:Now each class that wants to use the Personable module has complete flexibility to define
first_name
andlast_name
however it wishes, but the Personable module is completely agnostic about that. This is analogous tomatchingTodosSelector
being agnostic about how a component gets or provides avisibleTodos()
method.As for counting the number of places to look for a dependency --- I'm not sure I follow what you're trying to say.
It's true that a global selector intending to transform
state
data can "live" anywhere and be "connected" anywhere, without worrying about whether the component that wants to use it fulfils a certain implicit contract. This is because the actual "implicit" contract of such selectors is that they be called by something (connect
) that provides a particular object (state
).It's also true that a selector like
matchingTodosSelector()
is a different animal because it is expected to be passed a component that provides a certain API. Therefore, not just any component can use it (in the same way that not just any model in our Ruby app would usePersonable
).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.
Basically when I was describing "number of places to look for a dependency" I was talking about the number of spots we need to be sure conform to that implicit contract (exposing
this.props.todos
andthis.visibleTodos()
). I think relying on those implicit contracts is undesirable in this case, because it makes testing the functions that computevisibleTodos
andmatchingTodos
more difficult to test (they would arguably need to be tested in the context of every class/component that include them), and because it makes the code/thing/function that computesvisibleTodos
tightly coupled to the components that wantvisibleTodos
computed (the computing function needs knowledge of how the objects passed to it expose the data it needs, not to mention it expands the component's public API).I think it would be better to have "computations that a component may want to memoize" implemented as memoizing functions that don't need to know anything about the classes/components that use them (no implicit contracts, instead a more explicit contract in the form of args). The memoizing function would just take the pieces of data that are necessary for the computation as arguments, instead of binding/passing the function
this
, and expandingthis
's public API.So instead of:
it could be something more like:
The benefits of trading the passing-of-this for passing-in-args approach here are:
Here is a blog post related to
include
ing utility functions and creating higher order components as means of achieving composition.https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
NOTE: it turns out
lodash
has a nice function for creating memoizing functions:const square = _.memoize((a) => { return a^2; });
. For each value ofa
,square(a)
will only be computed once, and I think is roughly the same API we briefly thought might be possible with reselect.