Detailed changes
@@ -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",
@@ -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} />;
};
@@ -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);
@@ -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;
+}
@@ -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 />;
}
@@ -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
@@ -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"