Skip to content
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

Add registry param to withDispatch component #11851

Merged
merged 14 commits into from
Nov 30, 2018
Merged

Conversation

gziolo
Copy link
Member

@gziolo gziolo commented Nov 14, 2018

Description

In the majority of cases, it will be sufficient to use only two first params passed to mapDispatchToProps. However, there might be some very advanced use cases where using select function might be used as a tool to optimize the performance of your component. It is useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the withSelect higher order component to compute such prop, which might lead to unnecessary re-renders of you component caused by its frequent value change.

TODO

  • Update docs
  • Update code occurences which pass selectors as prop

How has this been tested?

Unit tests: npm run test

  1. Open an existing post (can be the demo).
  2. Select all blocks (cmd+a twice) and copy them using cmd+c.
  3. Paste them in the new empty post with cmd+v.
  4. Select all blocks again and cut them using cmd+x.
  5. Paste them in the new empty post with cmd+v.

Types of changes

Refactoring.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.

@gziolo gziolo added [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts [Package] Data /packages/data labels Nov 14, 2018
@gziolo gziolo added this to the 4.4 milestone Nov 14, 2018
@gziolo gziolo self-assigned this Nov 14, 2018
@gziolo gziolo removed the [Status] In Progress Tracking issues with work in progress label Nov 14, 2018
@aduth
Copy link
Member

aduth commented Nov 14, 2018

Like I'd mentioned at #11495 (comment), I'm not really a fan of the idea, because inevitably I think a developer would expect to be able to...

it( 'provides selected data as a prop?', () => {
	registry.registerStore( 'counter', {
		reducer( state = 0, action ) {
			if ( action.type === 'increment' ) {
				return state + 1;
			}
			return state;
		},
		actions: {
			increment: () => ( { type: 'increment' } ),
		},
		selectors: {
			getCount: ( state ) => state,
		},
	} );

	const Component = withDispatch( ( _dispatch, ownProps, _select ) => {
		const count = _select( 'counter' ).getCount();

		return {
			count,
			increment: _dispatch( 'counter' ).increment,
		};
	} )( ( props ) => (
		<button onClick={ props.increment }>
			{ props.count }
		</button>
	) );

	const testRenderer = TestRenderer.create(
		<RegistryProvider value={ registry }>
			<Component />
		</RegistryProvider>
	);

	registry.dispatch( 'counter' ).increment();

	expect( testRenderer.root.findByType( 'button' ).props.children ).toBe( 1 );
} );

@gziolo
Copy link
Member Author

gziolo commented Nov 14, 2018

Haha, you caught me. This is what I was planning to do:

diff --git a/packages/editor/src/components/copy-handler/index.js b/packages/editor/src/components/copy-handler/index.js
index e74cde91b..fb66758c3 100644
--- a/packages/editor/src/components/copy-handler/index.js
+++ b/packages/editor/src/components/copy-handler/index.js
@@ -26,18 +26,7 @@ class CopyHandler extends Component {
 	}
 
 	onCopy( event ) {
-		const { hasMultiSelection, selectedBlockClientIds, getBlocksByClientId } = this.props;
-
-		if ( selectedBlockClientIds.length === 0 ) {
-			return;
-		}
-
-		// Let native copy behaviour take over in input fields.
-		if ( ! hasMultiSelection && documentHasSelection() ) {
-			return;
-		}
-
-		const serialized = serialize( getBlocksByClientId( selectedBlockClientIds ) );
+		const serialized = serialize( this.props.getSelectedBlocks() );
 
 		event.clipboardData.setData( 'text/plain', serialized );
 		event.clipboardData.setData( 'text/html', serialized );
@@ -65,7 +54,6 @@ export default compose( [
 		const {
 			getMultiSelectedBlockClientIds,
 			getSelectedBlockClientId,
-			getBlocksByClientId,
 			hasMultiSelection,
 		} = select( 'core/editor' );
 
@@ -75,13 +63,28 @@ export default compose( [
 		return {
 			hasMultiSelection: hasMultiSelection(),
 			selectedBlockClientIds,
+		};
+	} ),
+	withDispatch( ( dispatch, ownProps, select ) => {
+		const { getBlocksByClientId } = select( 'core/editor' );
+		const { removeBlocks } = dispatch( 'core/editor' );
+
+		return {
+			getSelectedBlocks: function() {
+				const { hasMultiSelection, selectedBlockClientIds } = ownProps;
+
+				if ( selectedBlockClientIds.length === 0 ) {
+					return;
+				}
+
+				// Let native copy behaviour take over in input fields.
+				if ( ! hasMultiSelection && documentHasSelection() ) {
+					return;
+				}
 
-			// We only care about this value when the copy is performed
-			// We call it dynamically in the event handler to avoid unnecessary re-renders.
-			getBlocksByClientId,
+				return serialize( getBlocksByClientId( selectedBlockClientIds ) );
+			},
+			onRemove: removeBlocks,
 		};
 	} ),
-	withDispatch( ( dispatch ) => ( {
-		onRemove: dispatch( 'core/editor' ).removeBlocks,
-	} ) ),
 ] )( CopyHandler );

@aduth
Copy link
Member

aduth commented Nov 14, 2018

In considering this further, and in attention of a common pattern of components receiving props for the sole purpose of shuffling them through to the dispatch callback in the limited cases at which they are relevant (example), I'll retract my hesitation.

If we can assure that the properties of the return value of mapDispatchToProps are functions, we can have a reasonable guarantee that those functions, once called, can receive the latest state, due to the behavior of withDispatch in re-calling mapDispatchToProps at the time the function is called.

@gziolo
Copy link
Member Author

gziolo commented Nov 14, 2018

If we can assure that the properties of the return value of mapDispatchToProps are functions, we can have a reasonable guarantee that those functions, once called, can receive the latest state, due to the behavior of withDispatch in re-calling mapDispatchToProps at the time the function is called.

It has been addressed with 99d290b. I added unit test which ensures that function will throw an error when something other than function will throw an error.

@gziolo
Copy link
Member Author

gziolo commented Nov 14, 2018

https://github.com/WordPress/gutenberg/blob/master/packages/edit-post/src/components/header/index.js#L93

So this makes now editor explode :)
Isn't a nice way to find another place to apply this optimization? 😄
I will fix later today.

@gziolo
Copy link
Member Author

gziolo commented Nov 14, 2018

Added 6eb6826 which fixes the previous issue.

@youknowriad youknowriad modified the milestones: 4.4, 4.5 Nov 15, 2018
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less included refactoring is better if we want it merged sooner than later.

packages/data/src/components/with-dispatch/index.js Outdated Show resolved Hide resolved
@gziolo
Copy link
Member Author

gziolo commented Nov 16, 2018

@aduth, it's ready for another review.

@gziolo gziolo added the [Priority] Low Used to indicate that the issue at hand isn't a top priority to address and can be handled later label Nov 19, 2018
@catehstn
Copy link

Moving this to 4.6, no rush to get it in today.

@catehstn catehstn removed this from the 4.5 milestone Nov 19, 2018
@gziolo
Copy link
Member Author

gziolo commented Nov 29, 2018

Tested the latest changes with the following scenario:

  1. Open an existing post (can be the demo).
  2. Select all blocks (cmd+a twice) and copy them using cmd+c.
  3. Paste them in the new empty post with cmd+v.
  4. Select all blocks again and cut them using cmd+x.
  5. Paste them in the new empty post with cmd+v.

@gziolo gziolo merged commit 3ce3f03 into master Nov 30, 2018
@gziolo gziolo deleted the update/with-dispatch-select branch November 30, 2018 09:14
daniloercoli added a commit that referenced this pull request Nov 30, 2018
…HEAD

* 'master' of https://github.com/WordPress/gutenberg:
  [RNmobile] Fix problems with undo/redo on Android (#12417)
  Add registry param to withDispatch component (#11851)
  Autocompleters: Consider block category (#12287)
  Only init TinyMCE once per instance (#12386)
  RichText: convert HTML formatting whitespace to spaces (#12166)
  Notices: Remove "files" block in package.json (#12438)
  Edit Post: Avoid rendering AdminNotices compatibility component (#12444)
  Correct the docs manifest (#12411)
@@ -7622,7 +7622,7 @@
},
"eslint-plugin-react": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz",
"resolved": "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've been seeing it every now and then for a few weeks. It's very annoying behavior of npm. Probably some subtle bug recently introduced :(

@jsnajdr
Copy link
Member

jsnajdr commented Dec 14, 2018

@gziolo I know I'm very late to the party -- I found this PR when reviewing the @wordpress/* package upgrade in Calypso and wondering why the data package got a 4.0 to 4.1 bump -- but:

If an action needs to know the latest state, that's a very common use case for Redux thunks:

const startSale = () => ( dispatch, getState ) => {
  const stockNumber = getStockNumber( getState() );
  const discountPercent = stockNumber > 50 ? 10 : 20;
  return dispatch( { type: START_SALE, discountPercent } );
};

Then, using the startSale action creator and dispatching the result always uses the latest value in real time.

Just FYI about what the classic vanilla Redux solution is. I don't know if it would apply to the problem that inspired this PR, I just hope it would.

@gziolo
Copy link
Member Author

gziolo commented Dec 14, 2018

@jsnajdr - what you shared is another way of tackling that and would work for the example we have in the docs. The implementation we shipped was based on a particular use case which helped us to prevent re-render of all blocks when using arrow key presses. See the lines below:
https://github.com/WordPress/gutenberg/pull/11851/files#diff-8b707a252d7b6da3af477595c2ce11dcL687

To apply your proposal to onShiftSelection action, it seems like we would have to update two actions to make it work. Anyways, it might be useful to support both approaches. If you think it would be beneficial to inject state into actions feel free to open PR with a proposal 👍

@youknowriad
Copy link
Contributor

Actually, we already have a way to do that in actions with controls and generators.

@jsnajdr
Copy link
Member

jsnajdr commented Dec 14, 2018

Actually, we already have a way to do that in actions with controls and generators.

How would I do it? Create a synchronous control that selects from the registry? Something like this:

controls = {
  GET_STOCK_NUMBER() {
    return select( 'my/store' ).getStockNumber();
  }
};

function* startSale() {
  const stockNumber = yield { type: GET_STOCK_NUMBER };
  return { type: START_SALE, discountPercent };
}

The code I wrote might be completely misguided, I haven't used a control in practice yet 🙂

@youknowriad
Copy link
Contributor

I was thinking of a generic "select" control. Something like that is already used here

https://github.com/WordPress/gutenberg/blob/master/packages/core-data/src/entities.js#L83

@aduth
Copy link
Member

aduth commented Dec 14, 2018

I'm excited to start playing with this PR to improve performance of our components.

Follow-up task with some preliminary investigation: #12890

// <SaleButton discountPercent="20">Start Sale!</SaleButton>
```

In the majority of cases, it will be sufficient to use only two first params passed to `mapDispatchToProps` as illustrated in the previous example. However, there might be some very advanced use cases where using the `registry` object might be used as a tool to optimize the performance of your component. Using `select` function from the registry might be useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the `withSelect` higher order component to compute such prop, which might lead to unnecessary re-renders of you component caused by its frequent value change. Keep in mind, that `mapDispatchToProps` must return an object with functions only.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we lost this useful information in the transition toward docgen-generated documentation, cc @nosolosw

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how so? I see the comment in the readme taken from the JSDoc block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I... swear I'd seen this missing from the current documentation. But it's there, you're quite right. Sorry for falsely raising the alarms!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to be sorry, I'm happily porting anything that's missing :) We ought to have great docs!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data /packages/data [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants