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

Blocks: Add a data store to manage the block/categories registration #6679

Merged
merged 8 commits into from
May 14, 2018

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented May 10, 2018

This PR introduces a store to the blocks module, this allows things like:

  • using withSelect to retrieve blocks/categories...
  • reacting to block registration
  • pave the way to an addCategories action.

This has been on my mind for a long time now, but I thought it would be a good first step towards the refactoring needed to introduce the server-side block awareness (registerBlockType and implementBlockType). My idea is to update the retrieval of blockTypes etc... to rely on the selector and we could change the underlying implementation of the selector if we split block registration into "defining the existence of a block" and "implement the block in the editor".

At the moment, the external API remains the same, there's no deprecation or anything (I prefer to leave this for the server-side awareness work which would clarify the API needed). So basically, I just updated the existing APIs to call select/dispatch instead of relying on the local variables. So as a follow-up task, I'd like to update our current usage of the block module API and use withSelect instead

@youknowriad youknowriad added the Framework Issues related to broader framework topics, especially as it relates to javascript label May 10, 2018
@youknowriad youknowriad self-assigned this May 10, 2018
@youknowriad youknowriad requested a review from a team May 10, 2018 09:43
@@ -252,7 +230,7 @@ export function getBlockType( name ) {
* @return {Array} Block settings.
*/
export function getBlockTypes() {
return Object.values( blocks );
return select( 'core/blocks' ).getBlockTypes();
Copy link
Member

@jorgefilipecosta jorgefilipecosta May 10, 2018

Choose a reason for hiding this comment

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

getBlockTypes is used for example to get the transforms. So it seems we are saving function references in the state. Maybe we can just save the serializable block information in the state (title icon etc...), and have an external object that maps the block to the non-serializable information like functions in the transforms.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can make use of resolvers or something similar, so it looks like everything is in the store, and we can select and dispatch as we do now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I personally think it's ok to save non-serializable data in the state. The benefits outgrow the downsides. Having serializable information in state is only needed if you want to persist state or dispatch things over the network. I think the blocks store is not something we'd want to persist or dispatch over the network if we implement collaborative editing for instance.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I agree saving the function references in state simplifies the things. Most sources discourage saving non-serializable info state, but objectively it looks like the only thing we lose is the ability to persist and rehydrate data https://github.com/markerikson/redux/blob/create-faq-page/docs/FAQ.md#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state, so I'm ok with doing this tradeoff as I did not found other concrete disavantages.

Copy link
Member

Choose a reason for hiding this comment

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

I'm happy so long as it doesn't break the very useful time travelling feature in Redux DevTools. I just tested it then and it seems OK 🙂

export function addBlockTypes( blockTypes ) {
return {
type: 'ADD_BLOCK_TYPES',
blockTypes: castArray( blockTypes ),
Copy link
Member

Choose a reason for hiding this comment

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

I like the fact that this action receives multiple blocks. It opens the door to bulk registration, instead of dispatching one action per register block.

But, right now we call registerBlockType per block so an action is dispatched per block and at the start, we are dispatching many actions. Right after landing this, I think we should change our registration mechanism to make use of the possibility of using ADD_BLOCK_TYPES with multiple blocks. If a general approach does not look feasible at least, we can change our registerCoreBlocks to register all core blocks with a single action.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that sounds like a good enhancement. I didn't want to change the Public API yet because the work that needs to be done on the server-side awareness of blocks will inevitably change the API, so it's better to deprecate things once.

@youknowriad youknowriad changed the title Blocks: Add a data store to manager the block/categories registration Blocks: Add a data store to manage the block/categories registration May 10, 2018
Copy link
Member

@jorgefilipecosta jorgefilipecosta left a comment

Choose a reason for hiding this comment

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

This PR seems to work well I did not catch any problem. And I think this is a good step in the direction of using our data module is the unique source of data 👍
I would like to know if anyone has other thoughts on the use of redux to save function references, or if there is any downside we did not found.

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.

Seems like a nice refactoring.

@@ -190,7 +168,7 @@ export function unregisterBlockType( name ) {
* @param {string} name Block name.
*/
export function setUnknownTypeHandlerName( name ) {
unknownTypeHandlerName = name;
dispatch( 'core/blocks' ).setFallbackBlockType( name );
Copy link
Member

Choose a reason for hiding this comment

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

Why do we call it the "fallback type" now, but the current API refers to it as the "unknown type" ?

Copy link
Member

@aduth aduth May 11, 2018

Choose a reason for hiding this comment

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

Also, the choice of referring to it by "name" was intentional; type is the object, we're setting, storing, and retrieving it as a string.

Copy link
Contributor Author

@youknowriad youknowriad May 11, 2018

Choose a reason for hiding this comment

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

I feel like fallback is a better name for consistency with the defaultBlockType name.

so instead of unknownTypeHandlerName and defaultBlockTypeName
we would have fallbackBlockTypeName and defaultBlockTypeName.

I can update to add the "name" suffix to the actions as well, it's already used in the selectors.

Copy link
Member

@aduth aduth May 11, 2018

Choose a reason for hiding this comment

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

But then should we want to update the block API to expose setFallbackBlockTypeName as well? Also, is it xBlockTypeName or xBlockName ? Latter is less verbose and should be equally meaningful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I can use the latter. Though, I don't want to change the API for now, Let's avoid deprecations until we figure out server-side awareness.

@@ -209,7 +187,7 @@ export function getUnknownTypeHandlerName() {
* @param {string} name Block name.
*/
export function setDefaultBlockName( name ) {
defaultBlockName = name;
dispatch( 'core/blocks' ).setDefaultBlockType( name );
Copy link
Member

Choose a reason for hiding this comment

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

Same note type vs. name.

*
* @return {Object} Updated state.
*/
export function blockTypes( state = { types: [] }, action ) {
Copy link
Member

Choose a reason for hiding this comment

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

Seems like storing this as a deep object needlessly complicates things. Why not blockTypes as the object of blockName: blockType, and separate reducers for defaultBlockName, fallbackBlockName ? Or if we do want it to be nested, create via combineReducers ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So there are two questions here:

  • Why it's an array instead of an object: I wanted to keep the order of the registration similar to the order of getBlockTypes.

  • Why it's nested: I thought originally it was needed to be able to use part of the st To be able to reset the defaultBlockName and fallbackBlockName when we remove the block, but you're right it's not I'll update using combineReducers

return state.blockTypes.types;
}

export function getBlockType( state, name ) {
Copy link
Member

Choose a reason for hiding this comment

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

JSDoc.

Copy link
Member

Choose a reason for hiding this comment

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

We should optimize the reducer state to enable easier lookup by name (i.e. shape as object with block name as key).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not certain if it's a valid thing we want (keeping the order of the registered blocks) but that was the reasoning behind state as array.

Copy link
Member

Choose a reason for hiding this comment

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

If lookup of block type by name is a common operation (which I suspect it is), we'd want to optimize it in some form. Either memoization, or storing order as a separate state value.

{
	"blockTypes": {
		"core/paragraph": {
			"attributes": {}
		}
	},
	"blockOrder": [
		"core/paragraph"
	]
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, it looks like we we're not keeping the same order previously since we were using an object variable to hold the block list, I'm just going to use an object for now and let's see later if we really want to keep the order.

return null;
}
return state;
case 'SET_FALLBACK_BLOCK_NAME':
Copy link
Member

Choose a reason for hiding this comment

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

My mind goes to abstracting a higher-order reducer to encapsulate the common logic 😄

export function createBlockNameSetterReducer( setActionType ) {
	return ( state = null, action ) => {
		switch ( action.type ) {
			case 'REMOVE_BLOCK_TYPES':
				if ( action.names.indexOf( state ) !== -1 ) {
					return null;
				}
				return state;

			case setActionType:
				return action.name || null;
		}

		return state;
	};
}

export const defaultBlockName = createBlockNameSetterReducer( 'SET_DEFAULT_BLOCK_NAME' );

export const fallbackBlockName = createBlockNameSetterReducer( 'SET_FALLBACK_BLOCK_NAME' );

Maybe premature 😉

export function categories( state = DEFAULT_CATEGORIES, action ) {
if ( action.type === 'ADD_CATEGORIES' ) {
const addedSlugs = map( action.categories, ( category ) => category.slug );
const previousState = filter( state, ( category ) => addedSlugs.indexOf( category.slug ) === -1 );
Copy link
Member

Choose a reason for hiding this comment

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

It's not entirely obvious what's going on here. Is this something _.uniqBy would work well for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

uniqBy won't keep the order, while it's not important for blocks, it's important for categories to keep the same order in the inserter.

*/
export const getBlockTypes = createSelector(
( state ) => Object.values( state.blockTypes ),
( state ) => state.blockTypes
Copy link
Member

Choose a reason for hiding this comment

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

Minor: We've not updated yet, but rememo@3.0.0 requires the second argument function return an array:

https://github.com/aduth/rememo/blob/master/CHANGELOG.md

@@ -44,48 +44,32 @@ export function blockTypes( state = {}, action ) {
}

/**
* Reducer keeping track of the default block name.
* Higher-order Reducer creating a reducer keeping track of give block name.
Copy link
Member

Choose a reason for hiding this comment

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

It should be of the given block - I think.

@youknowriad youknowriad merged commit 09e2da3 into master May 14, 2018
@youknowriad youknowriad deleted the try/blocks-store branch May 14, 2018 07:52
@mtias mtias added this to the 2.9 milestone May 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Framework Issues related to broader framework topics, especially as it relates to javascript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants