-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Track the parent block to optimize hierarchy selectors #16392
Changes from 7 commits
f1726fb
2d3ea3a
705d0fd
afefd02
b21c2bf
9d97b1b
e05ab9e
6087e69
3002180
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,23 @@ function mapBlockOrder( blocks, rootClientId = '' ) { | |
return result; | ||
} | ||
|
||
/** | ||
* Given an array of blocks, returns an object where each key contains | ||
* the clientId of the block and the value is the parent of the block. | ||
* | ||
* @param {Array} blocks Blocks to map. | ||
* @param {?string} rootClientId Assumed root client ID. | ||
* | ||
* @return {Object} Block order map object. | ||
*/ | ||
function mapBlockParents( blocks, rootClientId = '' ) { | ||
return blocks.reduce( ( result, block ) => Object.assign( | ||
result, | ||
{ [ block.clientId ]: rootClientId }, | ||
mapBlockParents( block.innerBlocks, block.clientId ) | ||
), {} ); | ||
} | ||
|
||
/** | ||
* Helper method to iterate through all blocks, recursing into inner blocks, | ||
* applying a transformation function to each one. | ||
|
@@ -264,16 +281,38 @@ function withIgnoredBlockChange( reducer ) { | |
* @return {Function} Enhanced reducer function. | ||
*/ | ||
const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { | ||
if ( state && action.type === 'REMOVE_BLOCKS' ) { | ||
const clientIds = [ ...action.clientIds ]; | ||
const getAllChildren = ( clientIds ) => { | ||
let result = clientIds; | ||
for ( let i = 0; i < result.length; i++ ) { | ||
if ( ! state.order[ result[ i ] ] ) { | ||
continue; | ||
} | ||
|
||
// For each removed client ID, include its inner blocks to remove, | ||
// recursing into those so long as inner blocks exist. | ||
for ( let i = 0; i < clientIds.length; i++ ) { | ||
clientIds.push( ...state.order[ clientIds[ i ] ] ); | ||
if ( result === clientIds ) { | ||
result = [ ...result ]; | ||
} | ||
|
||
result.push( ...state.order[ result[ i ] ] ); | ||
} | ||
|
||
action = { ...action, clientIds }; | ||
return result; | ||
}; | ||
|
||
if ( state ) { | ||
switch ( action.type ) { | ||
case 'REMOVE_BLOCKS': | ||
action = { | ||
...action, | ||
removedClientIds: getAllChildren( action.clientIds ), | ||
}; | ||
break; | ||
case 'REPLACE_BLOCKS': | ||
action = { | ||
...action, | ||
replacedClientIds: getAllChildren( action.clientIds ), | ||
}; | ||
break; | ||
} | ||
} | ||
|
||
return reducer( state, action ); | ||
|
@@ -306,6 +345,10 @@ const withBlockReset = ( reducer ) => ( state, action ) => { | |
...omit( state.order, visibleClientIds ), | ||
...mapBlockOrder( action.blocks ), | ||
}, | ||
parents: { | ||
...omit( state.parents, visibleClientIds ), | ||
...mapBlockParents( action.blocks ), | ||
}, | ||
}; | ||
} | ||
|
||
|
@@ -441,12 +484,12 @@ export const blocks = flow( | |
} | ||
|
||
return { | ||
...omit( state, action.clientIds ), | ||
...omit( state, action.replacedClientIds ), | ||
...getFlattenedBlocksWithoutAttributes( action.blocks ), | ||
}; | ||
|
||
case 'REMOVE_BLOCKS': | ||
return omit( state, action.clientIds ); | ||
return omit( state, action.removedClientIds ); | ||
} | ||
|
||
return state; | ||
|
@@ -517,12 +560,12 @@ export const blocks = flow( | |
} | ||
|
||
return { | ||
...omit( state, action.clientIds ), | ||
...omit( state, action.replacedClientIds ), | ||
...getFlattenedBlockAttributes( action.blocks ), | ||
}; | ||
|
||
case 'REMOVE_BLOCKS': | ||
return omit( state, action.clientIds ); | ||
return omit( state, action.removedClientIds ); | ||
} | ||
|
||
return state; | ||
|
@@ -618,7 +661,7 @@ export const blocks = flow( | |
const mappedBlocks = mapBlockOrder( action.blocks ); | ||
|
||
return flow( [ | ||
( nextState ) => omit( nextState, clientIds ), | ||
( nextState ) => omit( nextState, action.replacedClientIds ), | ||
( nextState ) => ( { | ||
...nextState, | ||
...omit( mappedBlocks, '' ), | ||
|
@@ -645,17 +688,56 @@ export const blocks = flow( | |
case 'REMOVE_BLOCKS': | ||
return flow( [ | ||
// Remove inner block ordering for removed blocks | ||
( nextState ) => omit( nextState, action.clientIds ), | ||
( nextState ) => omit( nextState, action.removedClientIds ), | ||
|
||
// Remove deleted blocks from other blocks' orderings | ||
( nextState ) => mapValues( nextState, ( subState ) => ( | ||
without( subState, ...action.clientIds ) | ||
without( subState, ...action.removedClientIds ) | ||
) ), | ||
] )( state ); | ||
} | ||
|
||
return state; | ||
}, | ||
|
||
// While technically redundant data as the inverse of `order`, it serves as | ||
// an optimization for the selectors which derive the ancestry of a block. | ||
parents( state = {}, action ) { | ||
switch ( action.type ) { | ||
case 'RESET_BLOCKS': | ||
return mapBlockParents( action.blocks ); | ||
|
||
case 'RECEIVE_BLOCKS': | ||
return { | ||
...state, | ||
...mapBlockParents( action.blocks ), | ||
}; | ||
|
||
case 'INSERT_BLOCKS': | ||
return { | ||
...state, | ||
...mapBlockParents( action.blocks, action.rootClientId || '' ), | ||
}; | ||
|
||
case 'MOVE_BLOCK_TO_POSITION': { | ||
return { | ||
...state, | ||
[ action.clientId ]: action.toRootClientId || '', | ||
}; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: unneeded block around return statement. |
||
|
||
case 'REPLACE_BLOCKS': | ||
return { | ||
...omit( state, action.replacedClientIds ), | ||
...mapBlockParents( action.blocks, state[ action.clientIds[ 0 ] ] ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re: My previous comment, I guess we still reference the original There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically we need the parent of the replaced blocks to assign it as parent of the inserted blocks. |
||
}; | ||
|
||
case 'REMOVE_BLOCKS': | ||
return omit( state, action.removedClientIds ); | ||
} | ||
|
||
return state; | ||
}, | ||
} ); | ||
|
||
/** | ||
|
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.
Is there a specific reason we need to rename these properties from
clientIds
, rather than just "enhancing" the existing value to account for the cascade? Seems like it might make this a bit simpler to implement, since you wouldn't need theswitch
to vary the behavior.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've been caught off guard in several occasions because of this. The fact that the property don't contain the same value between reducers (selection and blocks), and also the difference between the action creator (which I tend to take a look at to understand the action) led me to this change.
It is still a different action but instead but I think the fact that we introduce a new key makes it less confusing.
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 get the motivation for this. It could, however, be pretty jarring for someone who notices the discrepancy and pulls up the definition of
removeBlocks
, since it clearly only returnsclientIds
in its payload.Perhaps update
removeBlocks
to make that relationship clearer?In alternative, if we're really strict about action types, it would be more "correct" to let this higher-order reducer divert from one action type to a new one:
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.
Alternatively, with controls implementation, these days we could probably incorporate this into the action creator itself, rather than as a reducer enhancer. That way the logic is consolidated to one place.