Optimised re-renders with sortable tiles

This commit is contained in:
Mitchell McCaffrey 2021-06-15 20:08:45 +10:00
parent 6b927b4456
commit bc97f4838a
6 changed files with 159 additions and 95 deletions

View File

@ -4,6 +4,7 @@ import MapTile from "./MapTile";
import MapTileGroup from "./MapTileGroup"; import MapTileGroup from "./MapTileGroup";
import SortableTiles from "../tile/SortableTiles"; import SortableTiles from "../tile/SortableTiles";
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
import { getGroupItems } from "../../helpers/group"; import { getGroupItems } from "../../helpers/group";
@ -57,7 +58,12 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) {
} }
} }
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />; return (
<>
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
</>
);
} }
export default MapTiles; export default MapTiles;

View File

@ -1,26 +1,24 @@
import React from "react"; import React from "react";
import { createPortal } from "react-dom";
import { DragOverlay } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable"; import { SortableContext } from "@dnd-kit/sortable";
import { animated, useSpring, config } from "react-spring";
import { Badge } from "theme-ui";
import { moveGroupsInto } from "../../helpers/group"; import { moveGroupsInto } from "../../helpers/group";
import { keyBy } from "../../helpers/shared"; import { keyBy } from "../../helpers/shared";
import Vector2 from "../../helpers/Vector2";
import SortableTile from "./SortableTile"; import SortableTile from "./SortableTile";
import { import {
useTileDrag, useTileDragId,
useTileDragCursor,
useTileOverGroupId,
BASE_SORTABLE_ID, BASE_SORTABLE_ID,
GROUP_SORTABLE_ID, GROUP_SORTABLE_ID,
GROUP_ID_PREFIX,
} from "../../contexts/TileDragContext"; } from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext"; import { useGroup } from "../../contexts/GroupContext";
function SortableTiles({ renderTile, subgroup }) { function SortableTiles({ renderTile, subgroup }) {
const { dragId, overId, dragCursor } = useTileDrag(); const dragId = useTileDragId();
const dragCursor = useTileDragCursor();
const overGroupId = useTileOverGroupId();
const { const {
groups, groups,
selectedGroupIds: allSelectedIds, selectedGroupIds: allSelectedIds,
@ -46,15 +44,6 @@ function SortableTiles({ renderTile, subgroup }) {
const disableSorting = (openGroupId && !subgroup) || filter; const disableSorting = (openGroupId && !subgroup) || filter;
const disableGrouping = subgroup || disableSorting || filter; const disableGrouping = subgroup || disableSorting || filter;
const dragBounce = useSpring({
transform: !!dragId ? "scale(0.9)" : "scale(1)",
config: config.wobbly,
position: "relative",
});
const overGroupId =
overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9);
function renderSortableGroup(group, selectedGroups) { function renderSortableGroup(group, selectedGroups) {
if (overGroupId === group.id && dragId && group.id !== dragId) { if (overGroupId === group.id && dragId && group.id !== dragId) {
// If dragging over a group render a preview of that group // If dragging over a group render a preview of that group
@ -68,57 +57,6 @@ function SortableTiles({ renderTile, subgroup }) {
return renderTile(group); return renderTile(group);
} }
function renderDragOverlays() {
let selectedIndices = selectedGroupIds.map((groupId) =>
activeGroups.findIndex((group) => group.id === groupId)
);
const activeIndex = activeGroups.findIndex((group) => group.id === dragId);
// Sort so the draging tile is the first element
selectedIndices = selectedIndices.sort((a, b) =>
a === activeIndex ? -1 : b === activeIndex ? 1 : 0
);
selectedIndices = selectedIndices.slice(0, 5);
let coords = selectedIndices.map(
(_, index) => new Vector2(5 * index, 5 * index)
);
// Reverse so the first element is rendered on top
selectedIndices = selectedIndices.reverse();
coords = coords.reverse();
const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
return selectedGroups.map((group, index) => (
<DragOverlay dropAnimation={null} key={group.id}>
<div
style={{
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
cursor: dragCursor,
}}
>
<animated.div style={dragBounce}>
{renderTile(group)}
{index === selectedIndices.length - 1 &&
selectedGroupIds.length > 1 && (
<Badge
sx={{
position: "absolute",
top: 0,
right: 0,
transform: "translate(25%, -25%)",
}}
>
{selectedGroupIds.length}
</Badge>
)}
</animated.div>
</div>
</DragOverlay>
));
}
function renderTiles() { function renderTiles() {
const groupsByIds = keyBy(activeGroups, "id"); const groupsByIds = keyBy(activeGroups, "id");
const selectedGroupIdsSet = new Set(selectedGroupIds); const selectedGroupIdsSet = new Set(selectedGroupIds);
@ -156,7 +94,6 @@ function SortableTiles({ renderTile, subgroup }) {
return ( return (
<SortableContext items={activeGroups} id={sortableId}> <SortableContext items={activeGroups} id={sortableId}>
{renderTiles()} {renderTiles()}
{createPortal(dragId && renderDragOverlays(), document.body)}
</SortableContext> </SortableContext>
); );
} }

View File

@ -0,0 +1,93 @@
import React from "react";
import { createPortal } from "react-dom";
import { DragOverlay } from "@dnd-kit/core";
import { animated, useSpring, config } from "react-spring";
import { Badge } from "theme-ui";
import Vector2 from "../../helpers/Vector2";
import { useTileDragId } from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
function SortableTilesDragOverlay({ renderTile, subgroup }) {
const dragId = useTileDragId();
const {
groups,
selectedGroupIds: allSelectedIds,
filter,
openGroupId,
openGroupItems,
filteredGroupItems,
} = useGroup();
const activeGroups = subgroup
? openGroupItems
: filter
? filteredGroupItems
: groups;
// Only populate selected groups if needed
let selectedGroupIds = [];
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
selectedGroupIds = allSelectedIds;
}
const dragBounce = useSpring({
transform: !!dragId ? "scale(0.9)" : "scale(1)",
config: config.wobbly,
position: "relative",
});
function renderDragOverlays() {
let selectedIndices = selectedGroupIds.map((groupId) =>
activeGroups.findIndex((group) => group.id === groupId)
);
const activeIndex = activeGroups.findIndex((group) => group.id === dragId);
// Sort so the draging tile is the first element
selectedIndices = selectedIndices.sort((a, b) =>
a === activeIndex ? -1 : b === activeIndex ? 1 : 0
);
selectedIndices = selectedIndices.slice(0, 5);
let coords = selectedIndices.map(
(_, index) => new Vector2(5 * index, 5 * index)
);
// Reverse so the first element is rendered on top
selectedIndices = selectedIndices.reverse();
coords = coords.reverse();
const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
return selectedGroups.map((group, index) => (
<DragOverlay dropAnimation={null} key={group.id}>
<div
style={{
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
}}
>
<animated.div style={dragBounce}>
{renderTile(group)}
{index === selectedIndices.length - 1 &&
selectedGroupIds.length > 1 && (
<Badge
sx={{
position: "absolute",
top: 0,
right: 0,
transform: "translate(25%, -25%)",
}}
>
{selectedGroupIds.length}
</Badge>
)}
</animated.div>
</div>
</DragOverlay>
));
}
return createPortal(dragId && renderDragOverlays(), document.body);
}
export default SortableTilesDragOverlay;

View File

@ -16,17 +16,14 @@ function Tile({
children, children,
}) { }) {
const [ref, inView] = useInView({ triggerOnce: true }); const [ref, inView] = useInView({ triggerOnce: true });
return ( return (
<Flex <Box
sx={{ sx={{
position: "relative", position: "relative",
width: "100%", width: "100%",
height: "0", height: "0",
paddingTop: "100%", paddingTop: "100%",
borderRadius: "4px", borderRadius: "4px",
justifyContent: "center",
alignItems: "center",
overflow: "hidden", overflow: "hidden",
userSelect: "none", userSelect: "none",
}} }}
@ -128,7 +125,7 @@ function Tile({
)} )}
</> </>
)} )}
</Flex> </Box>
); );
} }

View File

@ -5,6 +5,7 @@ import TokenTileGroup from "./TokenTileGroup";
import TokenHiddenBadge from "./TokenHiddenBadge"; import TokenHiddenBadge from "./TokenHiddenBadge";
import SortableTiles from "../tile/SortableTiles"; import SortableTiles from "../tile/SortableTiles";
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
import { getGroupItems } from "../../helpers/group"; import { getGroupItems } from "../../helpers/group";
@ -61,7 +62,12 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
} }
} }
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />; return (
<>
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
</>
);
} }
export default TokenTiles; export default TokenTiles;

View File

@ -1,4 +1,4 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext, useEffect } from "react";
import { import {
MouseSensor, MouseSensor,
TouchSensor, TouchSensor,
@ -16,7 +16,9 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
import usePreventSelect from "../hooks/usePreventSelect"; import usePreventSelect from "../hooks/usePreventSelect";
const TileDragContext = React.createContext(); const TileDragIdContext = React.createContext();
const TileOverGroupIdContext = React.createContext();
const TileDragCursorContext = React.createContext();
export const BASE_SORTABLE_ID = "__base__"; export const BASE_SORTABLE_ID = "__base__";
export const GROUP_SORTABLE_ID = "__group__"; export const GROUP_SORTABLE_ID = "__group__";
@ -61,7 +63,7 @@ export function TileDragProvider({
} = useGroup(); } = useGroup();
const mouseSensor = useSensor(MouseSensor, { const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 5 }, activationConstraint: { distance: 3 },
}); });
const touchSensor = useSensor(TouchSensor, { const touchSensor = useSensor(TouchSensor, {
activationConstraint: { delay: 250, tolerance: 5 }, activationConstraint: { delay: 250, tolerance: 5 },
@ -70,16 +72,23 @@ export function TileDragProvider({
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
const [dragId, setDragId] = useState(); const [dragId, setDragId] = useState(null);
const [overId, setOverId] = useState(); const [overId, setOverId] = useState(null);
const [dragCursor, setDragCursor] = useState("pointer"); const [dragCursor, setDragCursor] = useState("pointer");
const [preventSelect, resumeSelect] = usePreventSelect(); const [preventSelect, resumeSelect] = usePreventSelect();
const [overGroupId, setOverGroupId] = useState(null);
useEffect(() => {
setOverGroupId(
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
);
}, [overId]);
function handleDragStart(event) { function handleDragStart(event) {
const { active, over } = event; const { active, over } = event;
setDragId(active.id); setDragId(active.id);
setOverId(over?.id); setOverId(over?.id || null);
if (!selectedGroupIds.includes(active.id)) { if (!selectedGroupIds.includes(active.id)) {
onGroupSelect(active.id); onGroupSelect(active.id);
} }
@ -93,7 +102,7 @@ export function TileDragProvider({
function handleDragOver(event) { function handleDragOver(event) {
const { over } = event; const { over } = event;
setOverId(over?.id); setOverId(over?.id || null);
if (over) { if (over) {
if ( if (
over.id.startsWith(UNGROUP_ID) || over.id.startsWith(UNGROUP_ID) ||
@ -111,8 +120,8 @@ export function TileDragProvider({
function handleDragEnd(event) { function handleDragEnd(event) {
const { active, over, overlayNodeClientRect } = event; const { active, over, overlayNodeClientRect } = event;
setDragId(); setDragId(null);
setOverId(); setOverId(null);
setDragCursor("pointer"); setDragCursor("pointer");
if (active && over && active.id !== over.id) { if (active && over && active.id !== over.id) {
let selectedIndices = selectedGroupIds.map((groupId) => let selectedIndices = selectedGroupIds.map((groupId) =>
@ -165,8 +174,8 @@ export function TileDragProvider({
} }
function handleDragCancel(event) { function handleDragCancel(event) {
setDragId(); setDragId(null);
setOverId(); setOverId(null);
setDragCursor("pointer"); setDragCursor("pointer");
resumeSelect(); resumeSelect();
@ -210,8 +219,6 @@ export function TileDragProvider({
return closestCenter(otherRects, rect); return closestCenter(otherRects, rect);
} }
const value = { dragId, overId, dragCursor };
return ( return (
<DragContext <DragContext
onDragStart={handleDragStart} onDragStart={handleDragStart}
@ -221,19 +228,37 @@ export function TileDragProvider({
sensors={sensors} sensors={sensors}
collisionDetection={customCollisionDetection} collisionDetection={customCollisionDetection}
> >
<TileDragContext.Provider value={value}> <TileDragIdContext.Provider value={dragId}>
{children} <TileOverGroupIdContext.Provider value={overGroupId}>
</TileDragContext.Provider> <TileDragCursorContext.Provider value={dragCursor}>
{children}
</TileDragCursorContext.Provider>
</TileOverGroupIdContext.Provider>
</TileDragIdContext.Provider>
</DragContext> </DragContext>
); );
} }
export function useTileDrag() { export function useTileDragId() {
const context = useContext(TileDragContext); const context = useContext(TileDragIdContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider"); throw new Error("useTileDrag must be used within a TileDragProvider");
} }
return context; return context;
} }
export default TileDragContext; export function useTileOverGroupId() {
const context = useContext(TileOverGroupIdContext);
if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider");
}
return context;
}
export function useTileDragCursor() {
const context = useContext(TileDragCursorContext);
if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider");
}
return context;
}