React Components and Key Prop Usage
The current issue stems from the mount/unmount process of <GameBlock />
components in React.
Although a key
prop is being passed to the component, React is uncertain if it's rendering the same element.
If I were to pinpoint the cause of React's uncertainty, it would likely be attributed to:
- Altering the array sort using:
const previousSpace = gameBlocks[spaceIndex];
gameBlocks[spaceIndex] = gameBlocks[index];
gameBlocks[index] = previousSpace;
- Generating different virtual DOM outcomes based on the condition used for
isSpace
:
({ correctIndex, currentIndex, isSpace, isNextToSpace }) => isSpace ? null : ( <GameBlock ....
In most cases, remounting isn't a concern for applications as it occurs quickly. However, when incorporating animations, avoid remounts to prevent interference with CSS transitions.
To ensure React recognizes the same node without requiring a remount, maintain consistency in the virtual DOM between renders by preserving identical keys.
Achieving this involves straightforward list rendering without complex alterations, ensuring consistent key passages across renders.
Passing 'isSpace' Down
Instead of modifying rendered DOM nodes, prioritize retaining an equal count of nodes in list rendering with matching keys in the same sequence.
Simply pass down 'isSpace' and apply styling like display:none;
to achieve this goal.
<GameBlock
...
isSpace={isSpace}
...
>
const StyledGameBlock = styled.div<{ ....}>`
...
display: ${({isSpace})=> isSpace? 'none':'flex'};
...
`;
Maintaining Array Sort Consistency
React detects modifications to the gameBlocks
array due to keys being rearranged, triggering unmount/remount cycles of rendered <GameBlock/>
components.
To affirm that React views the array as unaltered, solely modify item properties within the list without changing the sorting order itself.
In your scenario, preserve all properties unchanged, only updating the currentIndex
for blocks subject to movement or swapping.
const onMove = useCallback(
(index) => {
const newSpaceIndex = gameBlocks[index].currentIndex; // space adopts clicked block's index.
const movedBlockNewIndex = gameBlocks[spaceIndex].currentIndex; // clicked block assumes space's index.
setState({
spaceIndex: spaceIndex, // space maintains constant position in array.
gameBlocks: gameBlocks.map((block) => {
const isMovingBlock = index === block.correctIndex; // identifies clicked block
const isSpaceBlock =
gameBlocks[spaceIndex].currentIndex === block.currentIndex; // identifies space block
let newCurrentIndex = block.currentIndex; // most blocks remain stationary
if (isMovingBlock) {
newCurrentIndex = movedBlockNewIndex; // moving block swaps with space
}
if (isSpaceBlock) {
newCurrentIndex = newSpaceIndex; // space swaps with moving block
}
return {
...block,
currentIndex: newCurrentIndex,
isNextToSpace: getIsNextToSpace(newCurrentIndex, newSpaceIndex)
};
})
});
},
[gameBlocks, spaceIndex]
);
...
// Ensure onMove is called with index of clicked block
() => onMove(correctIndex)
Changes are limited to altering the currentIndex
for clicked block and space.
Code Sandbox Examples:
Access sandbox example inspired by the provided sandbox link.
Closing Remarks: Your code readability and comprehension are commendable. Well done!