diff --git a/src/app/WardrobePage/OutfitControls.js b/src/app/WardrobePage/OutfitControls.js index 80e8d5e..b0c67be 100644 --- a/src/app/WardrobePage/OutfitControls.js +++ b/src/app/WardrobePage/OutfitControls.js @@ -27,7 +27,40 @@ import { Link } from "react-router-dom"; */ function OutfitControls({ outfitState, dispatchToOutfit }) { const [focusIsLocked, setFocusIsLocked] = React.useState(false); - const toast = useToast(); + const onLockFocus = React.useCallback(() => setFocusIsLocked(true), [ + setFocusIsLocked, + ]); + const onUnlockFocus = React.useCallback(() => setFocusIsLocked(false), [ + setFocusIsLocked, + ]); + + // HACK: As of 1.0.0-rc.0, Chakra's `toast` function rebuilds unnecessarily, + // which triggers unnecessary rebuilds of the `onSpeciesColorChange` + // callback, which causes the `React.memo` on `SpeciesColorPicker` to + // fail, which harms performance. But it seems to work just fine if we + // hold onto the first copy of the function we get! :/ + const _toast = useToast(); + // eslint-disable-next-line react-hooks/exhaustive-deps + const toast = React.useMemo(() => _toast, []); + + const onSpeciesColorChange = React.useCallback( + (species, color, isValid, closestPose) => { + if (isValid) { + dispatchToOutfit({ + type: "setSpeciesAndColor", + speciesId: species.id, + colorId: color.id, + pose: closestPose, + }); + } else { + toast({ + title: `We haven't seen a ${color.name} ${species.name} before! 😓`, + status: "warning", + }); + } + }, + [dispatchToOutfit, toast] + ); return ( { - if (isValid) { - dispatchToOutfit({ - type: "setSpeciesAndColor", - speciesId: species.id, - colorId: color.id, - pose: closestPose, - }); - } else { - toast({ - title: `We haven't seen a ${color.name} ${species.name} before! 😓`, - status: "warning", - }); - } - }} + onChange={onSpeciesColorChange} /> setFocusIsLocked(true)} - onUnlockFocus={() => setFocusIsLocked(false)} + onLockFocus={onLockFocus} + onUnlockFocus={onUnlockFocus} /> diff --git a/src/app/WardrobePage/PosePicker.js b/src/app/WardrobePage/PosePicker.js index db65bd0..8acff36 100644 --- a/src/app/WardrobePage/PosePicker.js +++ b/src/app/WardrobePage/PosePicker.js @@ -28,15 +28,30 @@ import twemojiSick from "../../images/twemoji/sick.svg"; import twemojiMasc from "../../images/twemoji/masc.svg"; import twemojiFem from "../../images/twemoji/fem.svg"; +/** + * PosePicker shows the pet poses available on the current species/color, and + * lets the user choose which want they want! + * + * NOTE: This component is memoized with React.memo. It's relatively expensive + * to re-render on every outfit change - the contents update even if the + * popover is closed! This makes wearing/unwearing items noticeably + * slower on lower-power devices. + * + * So, instead of using `outfitState` like most components, we specify + * exactly which props we need, so that `React.memo` can see the changes + * that matter, and skip updates that don't. + */ function PosePicker({ - outfitState, + speciesId, + colorId, + pose, dispatchToOutfit, onLockFocus, onUnlockFocus, }) { const theme = useTheme(); const checkedInputRef = React.useRef(); - const { loading, error, poseInfos } = usePoses(outfitState); + const { loading, error, poseInfos } = usePoses(speciesId, colorId); if (loading) { return null; @@ -98,13 +113,13 @@ function PosePicker({ isOpen && "is-open" )} > - {getEmotion(outfitState.pose) === "HAPPY" && ( + {getEmotion(pose) === "HAPPY" && ( )} - {getEmotion(outfitState.pose) === "SAD" && ( + {getEmotion(pose) === "SAD" && ( )} - {getEmotion(outfitState.pose) === "SICK" && ( + {getEmotion(pose) === "SICK" && ( )} @@ -345,9 +360,7 @@ function EmojiImage({ src, alt }) { return {alt}; } -function usePoses(outfitState) { - const { speciesId, colorId } = outfitState; - +function usePoses(speciesId, colorId, selectedPose) { const { loading, error, data } = useQuery( gql` query PosePicker($speciesId: ID!, $colorId: ID!) { @@ -370,7 +383,7 @@ function usePoses(outfitState) { return { ...appearance, isAvailable: Boolean(appearance), - isSelected: outfitState.pose === pose, + isSelected: selectedPose === pose, }; }; @@ -459,4 +472,4 @@ const transformsByBodyId = { default: "scale(2.5)", }; -export default PosePicker; +export default React.memo(PosePicker); diff --git a/src/app/components/SpeciesColorPicker.js b/src/app/components/SpeciesColorPicker.js index 0d6a953..d3f6351 100644 --- a/src/app/components/SpeciesColorPicker.js +++ b/src/app/components/SpeciesColorPicker.js @@ -10,6 +10,11 @@ import { Delay, useFetch } from "../util"; * * It preloads all species, colors, and valid species/color pairs; and then * ensures that the outfit is always in a valid state. + * + * NOTE: This component is memoized with React.memo. It's not the cheapest to + * re-render on every outfit change. This contributes to + * wearing/unwearing items being noticeably slower on lower-power + * devices. */ function SpeciesColorPicker({ speciesId, @@ -303,4 +308,4 @@ const closestPosesInOrder = { ], }; -export default SpeciesColorPicker; +export default React.memo(SpeciesColorPicker);