Prevent unneccesary rerenders of the board on change
This commit is contained in:
parent
6a43b5c0dc
commit
5cc9574332
|
@ -22,6 +22,7 @@
|
||||||
"myzod": "^1.0.0-alpha.9",
|
"myzod": "^1.0.0-alpha.9",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-hook-form": "^5.6.3",
|
"react-hook-form": "^5.6.3",
|
||||||
"react-scripts": "^3.4.1",
|
"react-scripts": "^3.4.1",
|
||||||
"react-use-websocket": "^2.0.1",
|
"react-use-websocket": "^2.0.1",
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ServerTimeProvider } from './hooks/useServerTime';
|
import { ServerTimeProvider } from './hooks';
|
||||||
import { Game, GameProps } from './pages/game';
|
import { Game, GameProps } from './pages/game';
|
||||||
import { Login } from './pages/login';
|
import { Login } from './pages/login';
|
||||||
import { StaticView } from './pages/staticView';
|
import { StaticView } from './pages/staticView';
|
||||||
|
|
||||||
export const App = (_props: {}) => {
|
export const App = (_props: {}) => {
|
||||||
const [gameProps, setGameProps] = React.useState<GameProps | undefined>();
|
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') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
const query = querystring.parse(window.location.search.substring(1));
|
const query = querystring.parse(window.location.search.substring(1));
|
||||||
|
@ -24,9 +26,5 @@ export const App = (_props: {}) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Login onLogin={onLogin} />;
|
||||||
<Login
|
|
||||||
onLogin={(roomID, nickname) => setGameProps({ roomID, nickname, leave: () => setGameProps(undefined) })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { Button, createStyles, makeStyles, Theme, Typography } from '@material-u
|
||||||
import { grey, orange, red } from '@material-ui/core/colors';
|
import { grey, orange, red } from '@material-ui/core/colors';
|
||||||
import { Fireworks } from 'fireworks/lib/react';
|
import { Fireworks } from 'fireworks/lib/react';
|
||||||
import * as React from '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 { StateBoard, StateTile } from '../protocol';
|
||||||
import { TeamHue, teamSpecs } from '../teams';
|
import { TeamHue, teamSpecs } from '../teams';
|
||||||
import { AspectDiv } from './aspectDiv';
|
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 {
|
interface TileProps {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
onClick: (row: number, col: number) => void;
|
||||||
tile: StateTile;
|
tile: StateTile;
|
||||||
onClick: () => void;
|
|
||||||
spymaster: boolean;
|
spymaster: boolean;
|
||||||
myTurn: boolean;
|
myTurn: boolean;
|
||||||
winner: 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 classes = useTileStyles();
|
||||||
|
|
||||||
const bombRevealed = !!(tile.revealed && tile.view?.bomb);
|
const bombRevealed = !!(tile.revealed && tile.view?.bomb);
|
||||||
const alreadyExploded = React.useRef(bombRevealed);
|
const alreadyExploded = React.useRef(bombRevealed);
|
||||||
const explode = bombRevealed && !alreadyExploded.current;
|
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 (
|
return (
|
||||||
<AspectDiv aspectRatio="75%">
|
<AspectDiv aspectRatio="75%">
|
||||||
|
@ -106,9 +124,9 @@ const Tile = ({ tile, onClick, spymaster, myTurn, winner }: TileProps) => {
|
||||||
type="button"
|
type="button"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
onClick={onClick}
|
onClick={reveal}
|
||||||
style={tileStyle(tile, spymaster)}
|
style={tileStyle(tile, spymaster)}
|
||||||
disabled={spymaster || !myTurn || winner || tile.revealed}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" className={classes.typo}>
|
<Typography variant="h6" className={classes.typo}>
|
||||||
{tile.word}
|
{tile.word}
|
||||||
|
@ -117,20 +135,13 @@ const Tile = ({ tile, onClick, spymaster, myTurn, winner }: TileProps) => {
|
||||||
{explode ? (
|
{explode ? (
|
||||||
<div className={classes.explosionWrapper}>
|
<div className={classes.explosionWrapper}>
|
||||||
<div className={classes.explosion}>
|
<div className={classes.explosion}>
|
||||||
<Fireworks
|
<Fireworks {...fireworksProps} />
|
||||||
{...{
|
|
||||||
interval: 0,
|
|
||||||
colors: [red[700], orange[800], grey[500]],
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</AspectDiv>
|
</AspectDiv>
|
||||||
);
|
);
|
||||||
};
|
}, isEqual);
|
||||||
|
|
||||||
export interface BoardProps {
|
export interface BoardProps {
|
||||||
words: StateBoard;
|
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);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -163,8 +174,10 @@ export const Board = (props: BoardProps) => {
|
||||||
arr.map((tile, col) => (
|
arr.map((tile, col) => (
|
||||||
<div key={row * props.words.length + col}>
|
<div key={row * props.words.length + col}>
|
||||||
<Tile
|
<Tile
|
||||||
|
row={row}
|
||||||
|
col={col}
|
||||||
|
onClick={props.onClick}
|
||||||
tile={tile}
|
tile={tile}
|
||||||
onClick={() => props.onClick(row, col)}
|
|
||||||
spymaster={props.spymaster}
|
spymaster={props.spymaster}
|
||||||
myTurn={props.myTurn}
|
myTurn={props.myTurn}
|
||||||
winner={props.winner}
|
winner={props.winner}
|
||||||
|
@ -174,4 +187,4 @@ export const Board = (props: BoardProps) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}, isEqual);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export interface ServerTime {
|
export interface ServerTime {
|
||||||
setOffset: (v: number) => void;
|
setOffset: (v: number) => void;
|
||||||
|
@ -16,3 +17,9 @@ export const ServerTimeProvider = (props: React.PropsWithChildren<{}>) => {
|
||||||
export function useServerTime() {
|
export function useServerTime() {
|
||||||
return React.useContext(Context);
|
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 { fail } from 'assert';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import useWebSocket from 'react-use-websocket';
|
||||||
import { v4 } from 'uuid';
|
import { DeepReadonly } from 'ts-essentials';
|
||||||
|
|
||||||
import { assertIsDefined, assertNever, noop, reloadOutdatedPage, websocketUrl } from '../common';
|
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 { version as codiesVersion } from '../metadata.json';
|
||||||
import { ClientNote, PartialClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
import { ClientNote, PartialClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
||||||
import { GameView, Sender } from './gameView';
|
import { GameView, Sender } from './gameView';
|
||||||
|
@ -175,8 +175,8 @@ export interface GameProps {
|
||||||
leave: () => void;
|
leave: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Game = (props: GameProps) => {
|
export const Game = (props: DeepReadonly<GameProps>) => {
|
||||||
const [playerID] = React.useState(v4);
|
const playerID = useStableUUID();
|
||||||
const nickname = React.useRef(props.nickname); // Preserve a nickname for use in reconnects.
|
const nickname = React.useRef(props.nickname); // Preserve a nickname for use in reconnects.
|
||||||
|
|
||||||
const syncTime = useSyncedServerTime();
|
const syncTime = useSyncedServerTime();
|
||||||
|
@ -184,6 +184,7 @@ export const Game = (props: GameProps) => {
|
||||||
|
|
||||||
const reducer = useStateReducer(sendJsonMessage);
|
const reducer = useStateReducer(sendJsonMessage);
|
||||||
const [state, dispatch] = React.useReducer(reducer, undefined);
|
const [state, dispatch] = React.useReducer(reducer, undefined);
|
||||||
|
const player = usePlayer(playerID, state);
|
||||||
const send = useSender(dispatch);
|
const send = useSender(dispatch);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -202,8 +203,6 @@ export const Game = (props: GameProps) => {
|
||||||
}
|
}
|
||||||
}, [lastJsonMessage]);
|
}, [lastJsonMessage]);
|
||||||
|
|
||||||
const player = usePlayer(playerID, state);
|
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import { isDefined, nameofFactory, noComplete } from '../common';
|
import { isDefined, nameofFactory, noComplete } from '../common';
|
||||||
import { Board } from '../components/board';
|
import { Board } from '../components/board';
|
||||||
import { ClipboardButton } from '../components/clipboard';
|
import { ClipboardButton } from '../components/clipboard';
|
||||||
import { useServerTime } from '../hooks/useServerTime';
|
import { useServerTime } from '../hooks';
|
||||||
import { State, StatePlayer, StateTimer, WordPack } from '../protocol';
|
import { State, StatePlayer, StateTimer, WordPack } from '../protocol';
|
||||||
import { teamSpecs } from '../teams';
|
import { teamSpecs } from '../teams';
|
||||||
|
|
||||||
|
@ -511,10 +511,11 @@ const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => {
|
||||||
|
|
||||||
const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => {
|
const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => {
|
||||||
const myTurn = state.turn === pTeam;
|
const myTurn = state.turn === pTeam;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Board
|
<Board
|
||||||
words={state.board}
|
words={state.board}
|
||||||
onClick={(row, col) => myTurn && !pState.spymaster && send.reveal(row, col)}
|
onClick={send.reveal}
|
||||||
spymaster={pState.spymaster}
|
spymaster={pState.spymaster}
|
||||||
myTurn={myTurn}
|
myTurn={myTurn}
|
||||||
winner={isDefined(state.winner)}
|
winner={isDefined(state.winner)}
|
||||||
|
@ -637,6 +638,15 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||||
sidebar: {
|
sidebar: {
|
||||||
gridArea: '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 (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<div
|
<div className={classes.leaveWrapper}>
|
||||||
style={{
|
<Button type="button" onClick={props.leave} startIcon={<ArrowBack />} className={classes.leaveButton}>
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
margin: '0.5rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="button" onClick={props.leave} startIcon={<ArrowBack />} style={{ marginRight: '0.5rem' }}>
|
|
||||||
Leave
|
Leave
|
||||||
</Button>
|
</Button>
|
||||||
<ClipboardButton
|
<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"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
|
||||||
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
|
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:
|
react-hook-form@^5.6.3:
|
||||||
version "5.7.2"
|
version "5.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac"
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac"
|
||||||
|
|
Loading…
Reference in New Issue