Skip to content

Data: Implement useSelect hooks corollary to withSelect #15473

@aduth

Description

@aduth

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 and withDispatch.

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:

  • It is assumed hooks are easier to become familiar and to work with than higher-order components (from the perspective of the consuming component).
  • There is significant overlap between the goals of hooks and most higher-order components such that there is always a graceful transition path. We were also considerate enough to have established a naming convention (with*) that adapts quite well to a hooks equivalent (use*).
    • To assure a consistent experience one way or the other, we should recommend that in the adoption of the hooks pattern, we provide hooks alternatives to all higher-order components currently available in packages offerings. Depending on a decision to consider higher-order components as legacy, this should be bidirectional (i.e. new hooks offered with an equivalent higher-order component).

Prior art:

Proposal:

@wordpress/data will implement and make available a new useSelect and useDispatch pair of hooks. These will largely overlap with withSelect and withDispatch 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 a storeName and mapSelectToReturnValue function. Likewise, useDispatch will accept storeName and/or mapSelectToReturnValue. The intention here is for interoperability with withSelect and withDispatch, which will likewise be updated to add support for storeName or optionally omit mapSelectToProps / mapDispatchToProps. For most usage, it would be assumed that a developer would pass the storeName 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.

function useSelect( storeName: string ): Object;
function useSelect( storeName: string, mapSelectorsToReturnValue: Function ): any;
function useSelect( mapSelectToReturnValue: Function ): any;
function useDispatch( storeName: string ): Object;
function useDispatch( storeName: string, mapDispatchersToReturnValue: Function ): any;
function useDispatch( mapDispatchToReturnValue: Function ): any;

Example of an adapted PostStatus component:

function PostStatus() {
	const { isEditorPanelOpened } = useSelect( 'core/edit-post' );
	const { toggleEditorPanelOpened } = useDispatch( 'core/edit-post' );

	return (
		<PanelBody 
			title={ __( 'Status & Visibility' ) }
			opened={ isEditorPanelOpened( 'post-status' ) }
			onToggle={ () => toggleEditorPanelOpened( 'post-status' ) }
			className="edit-post-post-status"
		>
			{ /* ... */ }
		</PanelBody>
	);
}

Considerations:

  • Since 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).
  • For better or worse, we lose the abstracted distinction of what's commonly referred to as Presentational and Container components, where previously PostStatus was a sort of visual component agnostic to the source of its isOpened and onTogglePanel props.

Compatibility:

As noted above, the argument signature should be made identical between withSelect / useSelect and withDispatch / useDispatch. Specifically:

  • A component could be wrapped as withSelect( 'core/editor' ) and receive as props all available selectors for a given store.
  • A component could be wrapped as withSelect( 'core/editor', mapSelectorsToProps ), and mapSelectorsToProps would be called with an object of selectors for the given store, expected to return a mapped object of props to provide with the component

The intention here is two-fold:

  • Optimize for the more direct access of selectors offered by the useSelect( storeName ) form
  • Enable opt-in performance enhancements made possible by known store dependencies of a component (limit component subscriber to be called only upon changes to relevant stores).

Unknowns:

  • The discussions in prior art of React Redux are extensive, and may include some useful guidance or potential implementation barriers
  • There are additional hurdles to the proposed performance optimization of limited subscribers, and it may not be worth investing in this direction without an understanding of whether those can be overcome.
  • Given the considerations above, whether there are alternative interfaces which could more gracefully offer access to values produced by a selector, while retaining consistency with useDispatch and compatibility with withSelect.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions