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 ;
}
-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);