: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* blocks that exist in the post, leaving blocks that exist only in state (e.g.
* reusable blocks and blocks controlled by inner blocks controllers) alone.
* @param {Function} reducer Original reducer function.
* @return {Function} Enhanced reducer function.
const withBlockReset = reducer => (state, action) => {
if (action.type === 'RESET_BLOCKS') {
byClientId: new Map(getFlattenedBlocksWithoutAttributes(action.blocks)),
attributes: new Map(getFlattenedBlockAttributes(action.blocks)),
order: mapBlockOrder(action.blocks),
parents: new Map(mapBlockParents(action.blocks)),
controlledInnerBlocks: {}
newState.tree = new Map(state?.tree);
updateBlockTreeForBlocks(newState, action.blocks);
innerBlocks: action.blocks.map(subBlock => newState.tree.get(subBlock.clientId))
return reducer(state, action);
* Higher-order reducer which targets the combined blocks reducer and handles
* the `REPLACE_INNER_BLOCKS` action. When dispatched, this action the state
* should become equivalent to the execution of a `REMOVE_BLOCKS` action
* containing all the child's of the root block followed by the execution of
* `INSERT_BLOCKS` with the new blocks.
* @param {Function} reducer Original reducer function.
* @return {Function} Enhanced reducer function.
const withReplaceInnerBlocks = reducer => (state, action) => {
if (action.type !== 'REPLACE_INNER_BLOCKS') {
return reducer(state, action);
// Finds every nested inner block controller. We must check the action blocks
// and not just the block parent state because some inner block controllers
// should be deleted if specified, whereas others should not be deleted. If
// a controlled should not be deleted, then we need to avoid deleting its
// inner blocks from the block state because its inner blocks will not be
// attached to the block in the action.
const nestedControllers = {};
if (Object.keys(state.controlledInnerBlocks).length) {
const stack = [...action.blocks];
stack.push(...innerBlocks);
if (!!state.controlledInnerBlocks[block.clientId]) {
nestedControllers[block.clientId] = true;
// The `keepControlledInnerBlocks` prop will keep the inner blocks of the
// marked block in the block state so that they can be reattached to the
// marked block when we re-insert everything a few lines below.
let stateAfterBlocksRemoval = state;
if (state.order.get(action.rootClientId)) {
stateAfterBlocksRemoval = reducer(stateAfterBlocksRemoval, {
keepControlledInnerBlocks: nestedControllers,
clientIds: state.order.get(action.rootClientId)
let stateAfterInsert = stateAfterBlocksRemoval;
if (action.blocks.length) {
stateAfterInsert = reducer(stateAfterInsert, {
// We need to re-attach the controlled inner blocks to the blocks tree and
// preserve their block order. Otherwise, an inner block controller's blocks
// will be deleted entirely from its entity.
const stateAfterInsertOrder = new Map(stateAfterInsert.order);
Object.keys(nestedControllers).forEach(key => {
if (state.order.get(key)) {
stateAfterInsertOrder.set(key, state.order.get(key));
stateAfterInsert.order = stateAfterInsertOrder;
stateAfterInsert.tree = new Map(stateAfterInsert.tree);
Object.keys(nestedControllers).forEach(_key => {
const key = `controlled||${_key}`;
if (state.tree.has(key)) {
stateAfterInsert.tree.set(key, state.tree.get(key));
* Higher-order reducer which targets the combined blocks reducer and handles
* the `SAVE_REUSABLE_BLOCK_SUCCESS` action. This action can't be handled by
* regular reducers and needs a higher-order reducer since it needs access to
* both `byClientId` and `attributes` simultaneously.
* @param {Function} reducer Original reducer function.
* @return {Function} Enhanced reducer function.
const withSaveReusableBlock = reducer => (state, action) => {
if (state && action.type === 'SAVE_REUSABLE_BLOCK_SUCCESS') {
// If a temporary reusable block is saved, we swap the temporary id with the final one.
state.attributes = new Map(state.attributes);
state.attributes.forEach((attributes, clientId) => {
} = state.byClientId.get(clientId);
if (name === 'core/block' && attributes.ref === id) {
state.attributes.set(clientId, {
return reducer(state, action);
* Higher-order reducer which removes blocks from state when switching parent block controlled state.
* @param {Function} reducer Original reducer function.
* @return {Function} Enhanced reducer function.
const withResetControlledBlocks = reducer => (state, action) => {
if (action.type === 'SET_HAS_CONTROLLED_INNER_BLOCKS') {
// when switching a block from controlled to uncontrolled or inverse,
// we need to remove its content first.
const tempState = reducer(state, {
type: 'REPLACE_INNER_BLOCKS',
rootClientId: action.clientId,
return reducer(tempState, action);
return reducer(state, action);
* Reducer returning the blocks state.
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
* @return {Object} Updated state.
const blocks = (0,external_wp_compose_namespaceObject.pipe)(external_wp_data_namespaceObject.combineReducers, withSaveReusableBlock,
// Needs to be before withBlockCache.
// Needs to be before withInnerBlocksRemoveCascade.
withInnerBlocksRemoveCascade, withReplaceInnerBlocks,
// Needs to be after withInnerBlocksRemoveCascade.
withBlockReset, withPersistentBlockChange, withIgnoredBlockChange, withResetControlledBlocks)({
// The state is using a Map instead of a plain object for performance reasons.
// You can run the "./test/performance.js" unit test to check the impact
// code changes can have on this reducer.
byClientId(state = new Map(), action) {
const newState = new Map(state);
getFlattenedBlocksWithoutAttributes(action.blocks).forEach(([key, value]) => {
newState.set(key, value);
// Ignore updates if block isn't known.
if (!state.has(action.clientId)) {
// Do nothing if only attributes change.
if (Object.values(changes).length === 0) {
const newState = new Map(state);
newState.set(action.clientId, {
...state.get(action.clientId),
case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const newState = new Map(state);
action.replacedClientIds.forEach(clientId => {
newState.delete(clientId);
getFlattenedBlocksWithoutAttributes(action.blocks).forEach(([key, value]) => {
newState.set(key, value);
case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const newState = new Map(state);
action.removedClientIds.forEach(clientId => {
newState.delete(clientId);
// The state is using a Map instead of a plain object for performance reasons.
// You can run the "./test/performance.js" unit test to check the impact
// code changes can have on this reducer.
attributes(state = new Map(), action) {
const newState = new Map(state);
getFlattenedBlockAttributes(action.blocks).forEach(([key, value]) => {
newState.set(key, value);
// Ignore updates if block isn't known or there are no attribute changes.
if (!state.get(action.clientId) || !action.updates.attributes) {
const newState = new Map(state);
newState.set(action.clientId, {
...state.get(action.clientId),
...action.updates.attributes
case 'SYNC_DERIVED_BLOCK_ATTRIBUTES':
case 'UPDATE_BLOCK_ATTRIBUTES':
// Avoid a state change if none of the block IDs are known.
if (action.clientIds.every(id => !state.get(id))) {
const newState = new Map(state);
for (const clientId of action.clientIds) {
const updatedAttributeEntries = Object.entries(action.uniqueByBlock ? action.attributes[clientId] : (_action$attributes = action.attributes) !== null && _action$attributes !== void 0 ? _action$attributes : {});
if (updatedAttributeEntries.length === 0) {
let hasUpdatedAttributes = false;
const existingAttributes = state.get(clientId);
const newAttributes = {};
updatedAttributeEntries.forEach(([key, value]) => {
if (existingAttributes[key] !== value) {
hasUpdatedAttributes = true;
newAttributes[key] = value;
hasChange = hasChange || hasUpdatedAttributes;
if (hasUpdatedAttributes) {
return hasChange ? newState : state;
case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const newState = new Map(state);
action.replacedClientIds.forEach(clientId => {
newState.delete(clientId);
getFlattenedBlockAttributes(action.blocks).forEach(([key, value]) => {
newState.set(key, value);
case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const newState = new Map(state);
action.removedClientIds.forEach(clientId => {
newState.delete(clientId);
// The state is using a Map instead of a plain object for performance reasons.
// You can run the "./test/performance.js" unit test to check the impact
// code changes can have on this reducer.
order(state = new Map(), action) {
const blockOrder = mapBlockOrder(action.blocks);
const newState = new Map(state);
blockOrder.forEach((order, clientId) => {
newState.set(clientId, order);
newState.set('', ((_state$get = state.get('')) !== null && _state$get !== void 0 ? _state$get : []).concat(blockOrder['']));
const subState = state.get(rootClientId) || [];
const mappedBlocks = mapBlockOrder(action.blocks, rootClientId);
const newState = new Map(state);
mappedBlocks.forEach((order, clientId) => {
newState.set(clientId, order);
newState.set(rootClientId, insertAt(subState, mappedBlocks.get(rootClientId), index));
case 'MOVE_BLOCKS_TO_POSITION':
index = state.get(toRootClientId).length
// Moving inside the same parent block.
if (fromRootClientId === toRootClientId) {
const subState = state.get(toRootClientId);
const fromIndex = subState.indexOf(clientIds[0]);
const newState = new Map(state);
newState.set(toRootClientId, moveTo(state.get(toRootClientId), fromIndex, index, clientIds.length));
// Moving from a parent block to another.
const newState = new Map(state);
newState.set(fromRootClientId, (_state$get$filter = state.get(fromRootClientId)?.filter(id => !clientIds.includes(id))) !== null && _state$get$filter !== void 0 ? _state$get$filter : []);
newState.set(toRootClientId, insertAt(state.get(toRootClientId), clientIds, index));
const firstClientId = clientIds[0];
const subState = state.get(rootClientId);
if (!subState.length || firstClientId === subState[0]) {
const firstIndex = subState.indexOf(firstClientId);
const newState = new Map(state);
newState.set(rootClientId, moveTo(subState, firstIndex, firstIndex - 1, clientIds.length));
const firstClientId = clientIds[0];
const lastClientId = clientIds[clientIds.length - 1];
const subState = state.get(rootClientId);
if (!subState.length || lastClientId === subState[subState.length - 1]) {
const firstIndex = subState.indexOf(firstClientId);
const newState = new Map(state);
newState.set(rootClientId, moveTo(subState, firstIndex, firstIndex + 1, clientIds.length));
case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const mappedBlocks = mapBlockOrder(action.blocks);
const newState = new Map(state);
action.replacedClientIds.forEach(clientId => {
newState.delete(clientId);
mappedBlocks.forEach((order, clientId) => {
newState.set(clientId, order);
newState.forEach((order, clientId) => {
const newSubOrder = Object.values(order).reduce((result, subClientId) => {
if (subClientId === clientIds[0]) {
return [...result, ...mappedBlocks.get('')];
if (clientIds.indexOf(subClientId) === -1) {
result.push(subClientId);
newState.set(clientId, newSubOrder);
case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN':
const newState = new Map(state);
// Remove inner block ordering for removed blocks.
action.removedClientIds.forEach(clientId => {
newState.delete(clientId);
newState.forEach((order, clientId) => {
const newSubOrder = (_order$filter = order?.filter(id => !action.removedClientIds.includes(id))) !== null && _order$filter !== void 0 ? _order$filter : [];
if (newSubOrder.length !== order.length) {
newState.set(clientId, newSubOrder);
// 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 = new Map(), action) {
const newState = new Map(state);
mapBlockParents(action.blocks).forEach(([key, value]) => {
newState.set(key, value);
const newState = new Map(state);
mapBlockParents(action.blocks, action.rootClientId || '').forEach(([key, value]) => {
newState.set(key, value);
case 'MOVE_BLOCKS_TO_POSITION':
const newState = new Map(state);
action.clientIds.forEach(id => {
newState.set(id, action.toRootClientId || '');