-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Data: Implement useSelect hooks corollary to withSelect #15473
Comments
A couple of initial thoughts (I'm sure more will come as I have time to ponder this more):
|
Yeah, I think that would be ideal, and true of any higher-order component we choose to adapt to a hook (and vice-versa). |
Some thoughts:
I'm not sure we should offer this possibility as this means the components rerenders everytime the store
Right now,
I think a dependencies array as a last argument makes sense, we won't have to handle the dependency management ourselves since it will most likely based on Implementation
|
Ah, that's a very critically important point to raise. In that case, I suppose we must have the logic occur in the mapping function, rendering (via updated state
While not explicit, the idea was that what was proposed for function useSelect( storeName: string ): Object;
function withSelect( storeName: string ): Function;
function useSelect( storeName: string, mapSelectorsToReturnValue: Function ): any;
function withSelect( storeName: string, mapSelectorsToProps: Function ): Function;
function useSelect( mapSelectToReturnValue: Function ): any;
function withSelect( mapSelectToProps: Function ): Function; Given what you've noted about the form with
What values are you proposing as members of the array? Are you thinking as an array of store names, to limit the callback to be triggered only on updates to those stores? |
No, not really I'm thinking of props/other component variables used in the |
I think the dependency array should work similarly to The way I was think providing a storeName (or names) arguments could work - eg something with this signature: function useSelect( storeName: array ) ...is that providing storeNames implies that you not only want the selectors from those stores but also only want to rerender on changes to those specific store states. I realize that we'd need some work done so that listeners could be subscribed to a specific store. |
I'm going to do an experimental pull because I think this should be relatively straightforward to do. Edit: Forgot, and was reminded that @aduth already did some experimentation on this so I closed the pull I created in favor of his (#13177). |
Hi guys, I see a few issues with the API proposed and would like to propose a different one. Proposal: /**
* This would re-render every time the store receives an update.
* I don't see this being used over the other signatures.
* It could also be composed from other hooks.
*/
function useSelect( storeName: string ): Object;
/**
* I don't see this being used over the other signatures.
* It could also be composed from other hooks.
*/
function useSelect( storeName: string, mapSelectorsToReturnValue: Function ): any;
/**
* This works, although it is missing an optional dependency array.
* I would also add an optional array of store names to subscribe to,
* now that `subscribe` supports it.
*/
function useSelect( mapSelectToReturnValue: Function ): any;
/**
* This works, but we can drop the store name and just return the function.
*/
function useDispatch( storeName: string ): Object;
/**
* Not necessary as action creators don't change like state does.
*/
function useDispatch( storeName: string, mapDispatchersToReturnValue: Function ): any;
/**
* Not necessary as action creators don't change like state does.
*/
function useDispatch( mapDispatchToReturnValue: Function ): any; Revision: /**
* Low-level hook that provides direct access to the registry.
* Used in the implementation of the other 2 hooks.
*/
function useRegistry(): Object;
/**
* Simple, 1-to-1 mapping with `withSelect`.
* Only re-renders when the output of `mapSelectToReturnValue` is different.
* `storeNames` lets you optionally subscribe to specific stores to avoid
* unnecessarily running `mapSelectToReturnValue`.
* `dependencies` lets you optionally memoize `mapSelectToReturnValue`
* until one of the values provided changes.
*/
function useSelect(
mapSelectToReturnValue: Function, storeNames: String [], dependencies: any []
): any;
/**
* Returns the `dispatch` function.
* Action creators shouldn't change, so this is sufficient.
*/
function useDispatch(): Function; I think this is as simple as you can get while still covering every use case and performance concern. Let me know your thoughts. As for this point:
This can be avoided with a bit of defensive coding, see https://react-redux.js.org/next/api/hooks#stale-props-and-zombie-children Also, note that the current implementation of |
@epiqueras thanks for the feedback have you seen #15512 by any chance? |
Hi @nerrad, I think it makes more sense to have a mapping function rather than a hook per selector. Hooks can't be called conditionally so that will break down whenever you want to call a selector based on/with the results of another selector. Also, that implementation re-renders every time the store updates, not just when the result of the selector changes. That is bad for performance and since selectors can trigger state updates, it can run into an infinite loop. It also doesn't handle the case when arguments change but the store has not updated. |
I think I agree with this proposal with just one remark. I see the dependencies as an argument that is going to be more commonly used than a stores argument. I'd personally switch these two arguments and I don't think at the moment there's huge value in having the
@nerrad I don't think hooks returning hooks is a good idea (do we have examples in the React community?). It feels too adventurous for me given the strictness of the hooks rules. In such a workd the inner hook implementation could change breaking the hooks rule? |
The comments on #15512 are along the lines of what I was already thinking (see my notes in the ticket). So it's great to get that confirmation. I mostly was coming at it from the perspective of exposing the selector hooks for simpler implementations where just a smaller number of selectors are needed in an implementation. As noted in the pull I do not think this should be the only usecase for useSelect.
I'm not sure the current iteration would hit that because the hooks aren't actually invoked in the factory. So rules of hooks can still be followed. It's more just exposing each selector individually as their own hooks. I also like the proposal currently in here. I agree with Riad having a stores argument is a bit unnecessary right now because we don't support store specific subscriptions yet internally. So it would make sense in that case to have dependencies as the second argument (leaving the option for later iterations to add a store argument once we support store specific subscriptions). |
@nerrad @youknowriad
The former will seldom be used for our use cases. The latter will be common, although, most of the time, neither will be used. Also, it's semantic for the hooks dependencies array to be the last parameter in all hooks. |
@epiqueras Store-specific subscriptions are certainly an optimization worth exploring, and have been explored in the past as well:
The primary issue (also explained in the discussion of these pull requests) is in how to properly track dependencies between stores. The discussion resulting from #13177 (and specifically #13177 (comment)) have some ideas for how this might be implemented in the future. However, I don't know that it'd ever be something the developer themselves would express, rather than being programmatically generated during the build step. Regardless whether that comes to fruition, I don't see it necessarily being incompatible with the proposed signature (though it is unclear whether it would become a separate, third argument, or somehow combined into the single |
@aduth Yeah, it's not incompatible. I was just making a case for
You can't combine them because the items are used as React Redux actually opted to let users memoize the selector themselves before passing it down, now that I think more about it, that makes more sense. So let's just drop the Here, I made a prototype, let me know what you think: https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630 |
Just an update, I've currently got a branch I'm working on locally that uses much of what @epiqueras provided in his gist (thanks!) and I'm experimenting with implementing the new hook in |
@nerrad Wouldn't it be easier to build these bindings on top of React Redux and get everything for free? |
I see two main flaws with the current model that are not easy to solve:
Using React Redux would solve these two issues and allow us to benefit from all the new optimizations and features that their maintainers are working on, including React Hooks support. I think the registry can use React Redux's |
@epiqueras We actually started the data module by relying on react-redux and changed that down the road. One of the main reasons is the fact that The second performance gain we operated is the "Async" mode. AFAIK, react-redux don't have a support for that kind of subscriptions and I'm not sure it will be easy to implement above |
@youknowriad Thanks for the background info!
This could still be done with
Won't this be taken care of by React Async Rendering? It could also be implemented at the store level. I.e. have the store keep a queue of dispatched actions, instead of having each component keep a queue of store updates. Also, why is that a queue? Shouldn't the component just render the latest update when it can? |
@epiqueras you might be interested in reading the discussions in that PR #13056
The answer is no, explanations here #13056 (comment) |
Do you have an example, curious to know more.
That's not what the async mode is about because we still want important components to render sync (the current block) while less-priority components can be delayed (unselected blocks) |
That makes sense, thanks.
Actually, for
This could be a parameter of the subscriber ( |
Also, there are some discussions about this in the thread. The idea is if we have 1000 |
Yeah, but the store is immutable. Why run all the intermediate updates and not just the latest one? |
Oh we run just the latest ones, this is a "special" queue that allows us to "replace" items in the queue if the come from the same component. |
A bit related issue raised by @iandunn in #15244 which might shed some lights on what developers expect:
It seems that React hooks make it possible to get very close to this proposal. |
They do. Outside of our work on implementing things like |
yup, forgot about this :) |
The introduction of hooks in React 16.8 provides a simple pattern for stateful behaviors, enabling encapsulation of lifecycle events and side effects. This aligns quite well to the existing pattern of data-bound components, currently expressed using the higher-order components
withSelect
andwithDispatch
.Advantages:
There are many purported advantages to a hook-based alternative, chief amongst them being that higher-order components can be confusing and verbose to use in a component. Furthermore, the introduction of an optional hooks paradigm allows some reevaluation of existing patterns, such as what had been discussed in #12877 and #13177 with store-specific subscriptions.
Deterrents:
Hooks are a new concept which requires some amount of onboarding to become comfortable working with. That there is already a pattern of higher-order components in the codebase should demand some intentionality to the implementation of hooks generally, since we risk confusion stemming from inconsistency and fragmentation.
It should be possible to dismiss these worries if we can grant:
with*
) that adapts quite well to a hooks equivalent (use*
).Prior art:
react-redux
:react-redux
diverge from its own API ofconnect
in offeringuseSelector
anduseActions
hooks. This paradigm overlaps quite well with thewithSelect
andwithDispatch
higher-order components of@wordpress/data
, as further reinforcement to the fact thatuseSelect
anduseDispatch
are both possible and ideal.@wordpress/data
operates at a higher-level of abstraction thanreact-redux
(operating through action-dispatchers and selectors rather than direct access ofstore.dispatch
andstate
).Proposal:
@wordpress/data
will implement and make available a newuseSelect
anduseDispatch
pair of hooks. These will largely overlap withwithSelect
andwithDispatch
higher-order components in providing access to a store's selectors / action dispatchers, including necessary subscription behaviors.useSelect
will accept one, the other, or both of astoreName
andmapSelectToReturnValue
function. Likewise,useDispatch
will acceptstoreName
and/ormapSelectToReturnValue
. The intention here is for interoperability withwithSelect
andwithDispatch
, which will likewise be updated to add support forstoreName
or optionally omitmapSelectToProps
/mapDispatchToProps
. For most usage, it would be assumed that a developer would pass thestoreName
argument and receive an object of selectors / action-dispatchers.Edit (2019-05-13): Per subsequent discussion in this issue, this proposal is not representative of the advised implementation. See #15473 (comment) and #15473 (comment) for more detail. The issue will be revised in the future with the alternative recommended implementation.
Example of an adapted
PostStatus
component:Considerations:
useSelect
returns selectors and not the results of selector values, there's a risk of one of (a) verbose logic in a separate variable assignment for the result of the selector calls or (b) not assigning the selector call result to a variable and instead calling the selector multiple times as necessary (a potential performance concern).PostStatus
was a sort of visual component agnostic to the source of itsisOpened
andonTogglePanel
props.Compatibility:
As noted above, the argument signature should be made identical between
withSelect
/useSelect
andwithDispatch
/useDispatch
. Specifically:withSelect( 'core/editor' )
and receive as props all available selectors for a given store.withSelect( 'core/editor', mapSelectorsToProps )
, andmapSelectorsToProps
would be called with an object of selectors for the given store, expected to return a mapped object of props to provide with the componentThe intention here is two-fold:
useSelect( storeName )
formUnknowns:
useDispatch
and compatibility withwithSelect
.The text was updated successfully, but these errors were encountered: