Prevent unneccesary rerenders of the board on change

This commit is contained in:
zikaeroh 2020-05-28 19:03:07 -07:00
parent 6a43b5c0dc
commit 5cc9574332
7 changed files with 66 additions and 40 deletions

View File

@ -22,6 +22,7 @@
"myzod": "^1.0.0-alpha.9",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^5.6.3",
"react-scripts": "^3.4.1",
"react-use-websocket": "^2.0.1",

View File

@ -1,13 +1,15 @@
import querystring from 'querystring';
import * as React from 'react';
import { ServerTimeProvider } from './hooks/useServerTime';
import { ServerTimeProvider } from './hooks';
import { Game, GameProps } from './pages/game';
import { Login } from './pages/login';
import { StaticView } from './pages/staticView';
export const App = (_props: {}) => {
const [gameProps, setGameProps] = React.useState<GameProps | undefined>();
const leave = React.useCallback(() => setGameProps(undefined), []);
const onLogin = React.useCallback((roomID, nickname) => setGameProps({ roomID, nickname, leave }), [leave]);
if (process.env.NODE_ENV === 'development') {
const query = querystring.parse(window.location.search.substring(1));
@ -24,9 +26,5 @@ export const App = (_props: {}) => {
);
}
return (
<Login
onLogin={(roomID, nickname) => setGameProps({ roomID, nickname, leave: () => setGameProps(undefined) })}
/>
);
return <Login onLogin={onLogin} />;
};

View File

@ -2,8 +2,9 @@ import { Button, createStyles, makeStyles, Theme, Typography } from '@material-u
import { grey, orange, red } from '@material-ui/core/colors';
import { Fireworks } from 'fireworks/lib/react';
import * as React from 'react';
import isEqual from 'react-fast-compare';
import { isDefined } from '../common';
import { isDefined, noop } from '../common';
import { StateBoard, StateTile } from '../protocol';
import { TeamHue, teamSpecs } from '../teams';
import { AspectDiv } from './aspectDiv';
@ -85,20 +86,37 @@ const useTileStyles = makeStyles((theme: Theme) =>
})
);
const fireworksProps = {
interval: 0,
colors: [red[700], orange[800], grey[500]],
x: 0,
y: 0,
};
interface TileProps {
row: number;
col: number;
onClick: (row: number, col: number) => void;
tile: StateTile;
onClick: () => void;
spymaster: boolean;
myTurn: boolean;
winner: boolean;
}
const Tile = ({ tile, onClick, spymaster, myTurn, winner }: TileProps) => {
const Tile = React.memo(function Tile({ row, col, onClick, tile, spymaster, myTurn, winner }: TileProps) {
const classes = useTileStyles();
const bombRevealed = !!(tile.revealed && tile.view?.bomb);
const alreadyExploded = React.useRef(bombRevealed);
const explode = bombRevealed && !alreadyExploded.current;
const disabled = spymaster || !myTurn || winner || tile.revealed;
const reveal = React.useMemo(() => {
if (disabled) {
return noop;
}
return () => onClick(row, col);
}, [disabled, row, col, onClick]);
return (
<AspectDiv aspectRatio="75%">
@ -106,9 +124,9 @@ const Tile = ({ tile, onClick, spymaster, myTurn, winner }: TileProps) => {
type="button"
variant="contained"
className={classes.button}
onClick={onClick}
onClick={reveal}
style={tileStyle(tile, spymaster)}
disabled={spymaster || !myTurn || winner || tile.revealed}
disabled={disabled}
>
<Typography variant="h6" className={classes.typo}>
{tile.word}
@ -117,20 +135,13 @@ const Tile = ({ tile, onClick, spymaster, myTurn, winner }: TileProps) => {
{explode ? (
<div className={classes.explosionWrapper}>
<div className={classes.explosion}>
<Fireworks
{...{
interval: 0,
colors: [red[700], orange[800], grey[500]],
x: 0,
y: 0,
}}
/>
<Fireworks {...fireworksProps} />
</div>
</div>
) : null}
</AspectDiv>
);
};
}, isEqual);
export interface BoardProps {
words: StateBoard;
@ -154,7 +165,7 @@ const useStyles = makeStyles((theme: Theme) =>
})
);
export const Board = (props: BoardProps) => {
export const Board = React.memo(function Board(props: BoardProps) {
const classes = useStyles(props);
return (
@ -163,8 +174,10 @@ export const Board = (props: BoardProps) => {
arr.map((tile, col) => (
<div key={row * props.words.length + col}>
<Tile
row={row}
col={col}
onClick={props.onClick}
tile={tile}
onClick={() => props.onClick(row, col)}
spymaster={props.spymaster}
myTurn={props.myTurn}
winner={props.winner}
@ -174,4 +187,4 @@ export const Board = (props: BoardProps) => {
)}
</div>
);
};
}, isEqual);

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import { v4 } from 'uuid';
export interface ServerTime {
setOffset: (v: number) => void;
@ -16,3 +17,9 @@ export const ServerTimeProvider = (props: React.PropsWithChildren<{}>) => {
export function useServerTime() {
return React.useContext(Context);
}
export function useStableUUID(): string {
const id = React.useRef<string | undefined>();
id.current = id.current ?? v4();
return id.current;
}

View File

@ -1,10 +1,10 @@
import { fail } from 'assert';
import * as React from 'react';
import useWebSocket from 'react-use-websocket';
import { v4 } from 'uuid';
import { DeepReadonly } from 'ts-essentials';
import { assertIsDefined, assertNever, noop, reloadOutdatedPage, websocketUrl } from '../common';
import { useServerTime } from '../hooks/useServerTime';
import { useServerTime, useStableUUID } from '../hooks';
import { version as codiesVersion } from '../metadata.json';
import { ClientNote, PartialClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
import { GameView, Sender } from './gameView';
@ -175,8 +175,8 @@ export interface GameProps {
leave: () => void;
}
export const Game = (props: GameProps) => {
const [playerID] = React.useState(v4);
export const Game = (props: DeepReadonly<GameProps>) => {
const playerID = useStableUUID();
const nickname = React.useRef(props.nickname); // Preserve a nickname for use in reconnects.
const syncTime = useSyncedServerTime();
@ -184,6 +184,7 @@ export const Game = (props: GameProps) => {
const reducer = useStateReducer(sendJsonMessage);
const [state, dispatch] = React.useReducer(reducer, undefined);
const player = usePlayer(playerID, state);
const send = useSender(dispatch);
React.useEffect(() => {
@ -202,8 +203,6 @@ export const Game = (props: GameProps) => {
}
}, [lastJsonMessage]);
const player = usePlayer(playerID, state);
if (!state) {
return <Loading />;
}

View File

@ -38,7 +38,7 @@ import { Controller, useForm } from 'react-hook-form';
import { isDefined, nameofFactory, noComplete } from '../common';
import { Board } from '../components/board';
import { ClipboardButton } from '../components/clipboard';
import { useServerTime } from '../hooks/useServerTime';
import { useServerTime } from '../hooks';
import { State, StatePlayer, StateTimer, WordPack } from '../protocol';
import { teamSpecs } from '../teams';
@ -511,10 +511,11 @@ const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => {
const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => {
const myTurn = state.turn === pTeam;
return (
<Board
words={state.board}
onClick={(row, col) => myTurn && !pState.spymaster && send.reveal(row, col)}
onClick={send.reveal}
spymaster={pState.spymaster}
myTurn={myTurn}
winner={isDefined(state.winner)}
@ -637,6 +638,15 @@ const useStyles = makeStyles((theme: Theme) =>
sidebar: {
gridArea: 'sidebar',
},
leaveWrapper: {
position: 'absolute',
top: 0,
left: 0,
margin: '0.5rem',
},
leaveButton: {
marginRight: '0.5rem',
},
})
);
@ -645,15 +655,8 @@ export const GameView = (props: GameViewProps) => {
return (
<div className={classes.root}>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
margin: '0.5rem',
}}
>
<Button type="button" onClick={props.leave} startIcon={<ArrowBack />} style={{ marginRight: '0.5rem' }}>
<div className={classes.leaveWrapper}>
<Button type="button" onClick={props.leave} startIcon={<ArrowBack />} className={classes.leaveButton}>
Leave
</Button>
<ClipboardButton

View File

@ -9037,6 +9037,11 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-hook-form@^5.6.3:
version "5.7.2"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac"