Optimize a few more components

zikaeroh created

Change summary

frontend/src/pages/gameView.tsx | 131 +++++++++++++++++++++++++---------
frontend/src/protocol/index.ts  |  17 ++--
2 files changed, 105 insertions(+), 43 deletions(-)

Detailed changes

frontend/src/pages/gameView.tsx 🔗

@@ -33,13 +33,15 @@ import isArray from 'lodash/isArray';
 import range from 'lodash/range';
 import { DropzoneDialog } from 'material-ui-dropzone';
 import * as React from 'react';
+import isEqual from 'react-fast-compare';
 import { Controller, useForm } from 'react-hook-form';
+import { DeepReadonly } from 'ts-essentials';
 
 import { isDefined, nameofFactory, noComplete } from '../common';
 import { Board } from '../components/board';
 import { ClipboardButton } from '../components/clipboard';
 import { useServerTime } from '../hooks';
-import { State, StatePlayer, StateTimer, WordPack } from '../protocol';
+import { State, StatePlayer, StateTeams, StateTimer, StateWordList, WordPack } from '../protocol';
 import { teamSpecs } from '../teams';
 
 export interface Sender {
@@ -58,15 +60,6 @@ export interface Sender {
     changeHideBomb: (HideBomb: boolean) => void;
 }
 
-export interface GameViewProps {
-    roomID: string;
-    leave: () => void;
-    send: Sender;
-    state: State;
-    pState: StatePlayer;
-    pTeam: number;
-}
-
 const useCenterStyles = makeStyles((_theme: Theme) =>
     createStyles({
         blink: {
@@ -351,14 +344,29 @@ const useSidebarStyles = makeStyles((_theme: Theme) =>
     })
 );
 
-const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => {
+interface SidebarProps {
+    send: Sender;
+    teams: StateTeams;
+    lists: StateWordList[];
+    pTeam: number;
+    playerID: string;
+    version: number;
+    timer: StateTimer | undefined | null;
+}
+
+const Sidebar = React.memo(function Sidebar({
+    send,
+    teams,
+    lists,
+    pTeam,
+    playerID,
+    version,
+    timer,
+}: DeepReadonly<SidebarProps>) {
     const classes = useSidebarStyles();
     const theme = useTheme();
     const nameShade = theme.palette.type === 'dark' ? 400 : 600;
 
-    const teams = state.teams;
-    const lists = state.lists;
-
     const wordCount = React.useMemo(
         () =>
             lists.reduce((curr, l) => {
@@ -408,7 +416,7 @@ const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => {
                                         gridRow: j + 2,
                                         gridColumn: i + 1,
                                         color: teamSpecs[i].hue[nameShade],
-                                        fontStyle: member.playerID === pState.playerID ? 'italic' : undefined,
+                                        fontStyle: member.playerID === playerID ? 'italic' : undefined,
                                     }}
                                 >
                                     {member.spymaster ? `[${member.nickname}]` : member.nickname}
@@ -500,14 +508,15 @@ const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => {
                     </>
                 )}
             </div>
-            {!isDefined(state.timer) ? null : (
+            {!isDefined(timer) ? null : (
                 <div style={{ textAlign: 'left', marginTop: '1rem' }}>
-                    <TimerSlider version={state.version} timer={state.timer} onCommit={send.changeTurnTime} />
+                    <TimerSlider version={version} timer={timer} onCommit={send.changeTurnTime} />
                 </div>
             )}
         </>
     );
-};
+},
+isEqual);
 
 const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => {
     const myTurn = state.turn === pTeam;
@@ -523,16 +532,44 @@ const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => {
     );
 };
 
-const Footer = ({ send, state, pState }: GameViewProps) => {
-    const end = isDefined(state.winner);
+const useFooterStyles = makeStyles((_theme: Theme) =>
+    createStyles({
+        root: {
+            display: 'flex',
+            justifyContent: 'space-between',
+            alignContent: 'flex-start',
+            flexWrap: 'wrap',
+        },
+        left: {
+            display: 'flex',
+            alignContent: 'flex-start',
+            flexWrap: 'wrap',
+        },
+        leftButton: {
+            marginBottom: '0.5rem',
+            marginRight: '0.5rem',
+        },
+    })
+);
+
+interface FooterProps {
+    send: Sender;
+    end: boolean;
+    spymaster: boolean;
+    hideBomb: boolean;
+    hasTimer: boolean;
+}
+
+const Footer = React.memo(function Footer({ send, end, spymaster, hideBomb, hasTimer }: FooterProps) {
+    const classes = useFooterStyles();
 
     return (
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignContent: 'flex-start', flexWrap: 'wrap' }}>
-            <div style={{ display: 'flex', alignContent: 'flex-start', flexWrap: 'wrap' }}>
-                <ButtonGroup variant="outlined" style={{ marginBottom: '0.5rem', marginRight: '0.5rem' }}>
+        <div className={classes.root}>
+            <div className={classes.left}>
+                <ButtonGroup variant="outlined" className={classes.leftButton}>
                     <Button
                         type="button"
-                        variant={pState.spymaster ? undefined : 'contained'}
+                        variant={spymaster ? undefined : 'contained'}
                         onClick={() => send.changeRole(false)}
                         startIcon={<Search />}
                         disabled={end}
@@ -541,7 +578,7 @@ const Footer = ({ send, state, pState }: GameViewProps) => {
                     </Button>
                     <Button
                         type="button"
-                        variant={pState.spymaster ? 'contained' : undefined}
+                        variant={spymaster ? 'contained' : undefined}
                         onClick={() => send.changeRole(true)}
                         startIcon={<Person />}
                         disabled={end}
@@ -549,10 +586,10 @@ const Footer = ({ send, state, pState }: GameViewProps) => {
                         Spymaster
                     </Button>
                 </ButtonGroup>
-                <ButtonGroup variant="outlined" style={{ marginBottom: '0.5rem', marginRight: '0.5rem' }}>
+                <ButtonGroup variant="outlined" className={classes.leftButton}>
                     <Button
                         type="button"
-                        variant={state.hideBomb ? undefined : 'contained'}
+                        variant={hideBomb ? undefined : 'contained'}
                         onClick={() => send.changeHideBomb(false)}
                         startIcon={<Visibility />}
                     >
@@ -560,24 +597,24 @@ const Footer = ({ send, state, pState }: GameViewProps) => {
                     </Button>
                     <Button
                         type="button"
-                        variant={state.hideBomb ? 'contained' : undefined}
+                        variant={hideBomb ? 'contained' : undefined}
                         onClick={() => send.changeHideBomb(true)}
                         startIcon={<VisibilityOff />}
                     >
                         Hide bomb
                     </Button>
                 </ButtonGroup>
-                <ButtonGroup variant="outlined" style={{ marginBottom: '0.5rem', marginRight: '0.5rem' }}>
+                <ButtonGroup variant="outlined" className={classes.leftButton}>
                     <Button
                         type="button"
-                        variant={isDefined(state.timer) ? undefined : 'contained'}
+                        variant={hasTimer ? undefined : 'contained'}
                         onClick={() => send.changeTurnMode(false)}
                     >
                         <TimerOff />
                     </Button>
                     <Button
                         type="button"
-                        variant={isDefined(state.timer) ? 'contained' : undefined}
+                        variant={hasTimer ? 'contained' : undefined}
                         onClick={() => send.changeTurnMode(true)}
                     >
                         <Timer />
@@ -597,7 +634,7 @@ const Footer = ({ send, state, pState }: GameViewProps) => {
             </div>
         </div>
     );
-};
+}, isEqual);
 
 const useStyles = makeStyles((theme: Theme) =>
     createStyles({
@@ -650,8 +687,18 @@ const useStyles = makeStyles((theme: Theme) =>
     })
 );
 
-export const GameView = (props: GameViewProps) => {
+export interface GameViewProps {
+    roomID: string;
+    leave: () => void;
+    send: Sender;
+    state: State;
+    pState: StatePlayer;
+    pTeam: number;
+}
+
+export const GameView = (props: DeepReadonly<GameViewProps>) => {
     const classes = useStyles();
+    const end = isDefined(props.state.winner);
 
     return (
         <div className={classes.root}>
@@ -673,10 +720,24 @@ export const GameView = (props: GameViewProps) => {
                     <Board2 {...props} />
                 </div>
                 <div className={classes.footer}>
-                    <Footer {...props} />
+                    <Footer
+                        send={props.send}
+                        end={end}
+                        spymaster={props.pState.spymaster}
+                        hideBomb={props.state.hideBomb}
+                        hasTimer={isDefined(props.state.timer)}
+                    />
                 </div>
                 <div className={classes.sidebar}>
-                    <Sidebar {...props} />
+                    <Sidebar
+                        send={props.send}
+                        teams={props.state.teams}
+                        lists={props.state.lists}
+                        pTeam={props.pTeam}
+                        playerID={props.pState.playerID}
+                        version={props.state.version}
+                        timer={props.state.timer}
+                    />
                 </div>
             </div>
         </div>

frontend/src/protocol/index.ts 🔗

@@ -123,6 +123,14 @@ const StateTimer = myzod.object({
     turnEnd: myzod.date(),
 });
 
+export type StateWordList = DeepReadonly<Infer<typeof StateWordList>>;
+const StateWordList = myzod.object({
+    name: myzod.string(),
+    count: myzod.number(),
+    custom: myzod.boolean(),
+    enabled: myzod.boolean(),
+});
+
 export type State = DeepReadonly<Infer<typeof State>>;
 export const State = myzod.object({
     version: myzod.number(),
@@ -131,14 +139,7 @@ export const State = myzod.object({
     winner: myzod.number().optional().nullable(),
     board: StateBoard,
     wordsLeft: myzod.array(myzod.number()),
-    lists: myzod.array(
-        myzod.object({
-            name: myzod.string(),
-            count: myzod.number(),
-            custom: myzod.boolean(),
-            enabled: myzod.boolean(),
-        })
-    ),
+    lists: myzod.array(StateWordList),
     timer: StateTimer.optional().nullable(),
     hideBomb: myzod.boolean(),
 });