setState is delayed by one state - reactjs

Don't know what I am doing wrong here, maybe it is a rookie mistake.
I am trying to recreate chess.
My state is delayed by one.. i.e. when I click on square to show possible moves of piece, the possible moves will appear but only after my second click, they will show on the right spot. And for the 3rd click the state from 2nd click will appear on the right spot and so on.
What am I doing wrong here?
I am sending few code examples from my next.js app.
This is my Square component
export default function Square({
x,
y,
color,
content,
contentClr,
handleClick,
}) {
const col = color == 'light' ? '#F3D9DC' : '#744253';
return (
<div
className='square'
x={x}
y={y}
style={{ backgroundColor: col }}
onClick={() => handleClick(x, y)}
>
{content != '' && (
<h1 style={{ color: contentClr == 'light' ? 'white' : 'black' }}>
{content}
</h1>
)}
</div>
);
}
This is my Board component
import { useState, useEffect } from 'react';
import Square from './square';
import styles from '../styles/Board.module.css';
import rookMoves from './possibleMoves/rookMoves';
export default function Board({ initialBoard, lightTurn, toggleTurn }) {
let size = 8;
const [board, setBoard] = useState(initialBoard);
const [possibleMoves, setPossibleMoves] = useState([]);
//possibleMoves .. each possible move is represented as x, y coordinates.
useEffect(() => {
console.log('board state changed');
}, [board]);
useEffect(() => {
console.log('possible moves state changed');
console.log(possibleMoves);
renderPossibleMoves();
}, [possibleMoves]);
const playerClickOnPiece = (x, y) => {
let pos = getPossibleMoves(x, y, rookMoves, [left, right, up, down]);
setPossibleMoves(pos);
};
//where to is object that has functions for directions in it
const getPossibleMoves = (x, y, movePiece, whereTo) => {
let posMoves = [];
for (let i = 0; i < whereTo.length; i++) {
posMoves = posMoves.concat(movePiece(board, x, y, whereTo[i]));
}
console.log(posMoves);
return posMoves;
};
const renderPossibleMoves = () => {
console.log('board from renderPossibleMoves ', board);
let newBoard = board;
for (let i = 0; i < possibleMoves.length; i++) {
newBoard[possibleMoves[i].y][possibleMoves[i].x] = 10;
}
setBoard(newBoard);
};
const squareColor = (x, y) => {
return (x % 2 == 0 && y % 2 == 0) || (x % 2 == 1 && y % 2 == 1)
? 'light'
: 'dark';
};
const handleClick = (x, y) => {
if (lightTurn && board[y][x] > 0) {
console.log(
`show me possible moves of light ${board[y][x]} on ${x} ${y}`
);
playerClickOnPiece(x, y);
} else if (!lightTurn && board[y][x] < 0) {
console.log(`show me possible moves of dark ${board[y][x]} on ${x} ${y}`);
playerClickOnPiece(x, y);
} else if (board[y][x] == 'Pm') {
playerMove();
}
};
return (
<>
<div className='board'>
{board.map((row, y) => {
return (
<div className='row' key={y}>
{row.map((square, x) => {
return (
<Square
x={x}
y={y}
key={x}
color={squareColor(x, y)}
content={square}
contentClr={square > 0 ? 'light' : 'dark'}
handleClick={handleClick}
playerMove={toggleTurn}
/>
);
})}
</div>
);
})}
</div>
</>
);
}
import { useState } from 'react';
import Board from './board';
let initialBoard = [
[-4, -3, -2, -5, -6, -2, -3, -4],
[-1, -1, -1, -1, -1, -1, -1, -1],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1],
[4, 3, 2, 5, 6, 2, 3, 4],
];
export default function Game() {
const [lightTurn, setLightTurn] = useState(true);
const toggleTurn = () => {
setLightTurn(!lightTurn);
};
return (
<Board
initialBoard={initialBoard}
lightTurn={lightTurn}
toggleTurn={toggleTurn}
/>
);
}
I am sending an example that shows only possible moves with rook but if u click on any light piece.

#juliomalves's comment helped!
I was mutating state directly in renderPossibleMoves. Spread operator solved the issue.
from
let newBoard = board;
to
let newBoard = [...board];

Related

What makes the component re-render?

I have trouble finding the reason for making the component re-render when scrolling down.
I've tried to build scenes with particles using shader and FBO techniques.
I used useRef and useMemo to prevent re-rendering instead of useState. I tried to load 3d models in this component using useGLTF, but not worked.
Re-render happens when scrolling down.
What seems to be the problem?
Here is my code.
let opacity;
const size = 80;
const FboParticles = ({ models }) => {
const { viewport } = useThree();
const scroll = useScroll();
/**
* Particles options
*/
// This reference gives us direct access to our points
const points = useRef();
const simulationMaterialRef = useRef();
// Create a camera and a scene for our FBO
// Create a simple square geometry with custom uv and positions attributes
const [scene, camera, positions, uvs] = useMemo(() => {
return [
new Scene(),
new OrthographicCamera(-1, 1, 1, -1, 1 / Math.pow(2, 53), 1),
new Float32Array([
-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0,
]),
new Float32Array([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0]),
];
}, []);
// Create our FBO render target
const renderTarget = useFBO(size, size, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RGBAFormat,
stencilBuffer: false,
type: FloatType,
});
// Generate a "buffer" of vertex of size "size" with normalized coordinates
const particlesPosition = useMemo(() => {
const length = size * size;
const particles = new Float32Array(length * 3);
for (let i = 0; i < length; i++) {
let i3 = i * 3;
particles[i3 + 0] = (i % size) / size;
particles[i3 + 1] = i / size / size;
}
return particles;
}, []);
const uniforms = useMemo(
() => ({
uPositions: {
value: null,
},
uMobile: {
value: 1,
},
uPixelRatio: {
value: Math.min(window.devicePixelRatio, 2),
},
vColor: {
value: 0,
},
defaultTime: {
value: 0,
},
uOpacity: {
value: 0,
},
uColorTrigger: {
value: 0,
},
}),
[]
);
useLayoutEffect(() => {
if (viewport.width < 4.8) {
points.current.material.uniforms.uMobile.value = 0.1;
}
}, []);
// FBO useFrame
useFrame(({ gl, clock }) => {
gl.setRenderTarget(renderTarget);
gl.clear();
gl.render(scene, camera);
gl.setRenderTarget(null);
points.current.material.uniforms.uPositions.value = renderTarget.texture;
points.current.material.uniforms.defaultTime.value = clock.elapsedTime;
simulationMaterialRef.current.uniforms.uTime.value = clock.elapsedTime;
});
const [scales, colors] = useMemo(() => {
const length = size * size * 3;
const color = new Color();
const sca = [];
const cols = [];
let q = ["white", "white", 0x2675ad, 0x0b5394, 0x0b9490];
const range = viewport.width < 4.8 ? 20 : 40;
for (let i = 0; i < length; i++) {
const i3 = i * 3;
// color
color.set(q[randInt(0, 4)]);
cols[i3 + 0] = color.r;
cols[i3 + 1] = color.g;
cols[i3 + 2] = color.b;
// particles scale
sca[i] = Math.random() * range;
}
return [new Float32Array(sca), new Float32Array(cols)];
}, []);
/**
* mouse event
*/
const hoveredRef = useRef();
const planeGeo = useMemo(() => {
return new PlaneGeometry(viewport.width, viewport.height, 1, 1);
}, []);
/**
* scroll
*/
// scroll animation
useFrame(({ mouse }) => {
const x = (mouse.x * viewport.width) / 2;
const y = (mouse.y * viewport.height) / 2;
if (viewport.width > 6.7) {
if (hoveredRef.current) {
simulationMaterialRef.current.uniforms.uMouse.value = new Vector2(x, y);
simulationMaterialRef.current.uniforms.uMouseTrigger.value = 1;
} else simulationMaterialRef.current.uniforms.uMouseTrigger.value = 0;
}
const aRange = scroll.range(0.0, 1 / 12);
const bRange = scroll.range(0.7 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerA.value = aRange;
const cRange = scroll.range(1.7 / 12, 1 / 12);
const dRange = scroll.range(3.6 / 12, 1 / 12);
const c = scroll.visible(1.7 / 12, 1 / 12);
const d = scroll.visible(2.7 / 12, 1.9 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerB.value = cRange;
const e = scroll.visible(4.8 / 12, 2 / 12);
const eRange = scroll.range(4.8 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerC.value = eRange;
const f = scroll.visible(6.8 / 12, 1 / 12);
const g = scroll.visible(7.6 / 12, 1 / 12);
const fRange = scroll.range(6.8 / 12, 1 / 12);
const gRange = scroll.range(7.6 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerD.value = fRange;
simulationMaterialRef.current.uniforms.scrollTriggerE.value = gRange;
const h = scroll.visible(9.6 / 12, 2.4 / 12);
const hRange = scroll.range(9.6 / 12, 1 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerF.value = hRange;
points.current.material.uniforms.uColorTrigger.value = hRange;
const iRange = scroll.range(10.6 / 12, 1.4 / 12);
simulationMaterialRef.current.uniforms.scrollTriggerG.value = iRange;
// opacity
opacity = 1 - bRange;
c && (opacity = cRange);
d && (opacity = 1 - dRange);
e && (opacity = eRange);
f && (opacity = 1 - fRange);
g && (opacity = fRange);
h && (opacity = hRange);
points.current.material.uniforms.uOpacity.value = opacity;
});
return (
<>
<mesh
position={[0, 0, 0]}
onPointerOver={(e) => (
e.stopPropagation(), (hoveredRef.current = true)
)}
onPointerOut={(e) => (hoveredRef.current = false)}
visible={false}
geometry={planeGeo}
/>
{/* Render off-screen our simulation material and square geometry */}
{createPortal(
<mesh>
<simulationMaterial
ref={simulationMaterialRef}
args={[size, viewport, models]}
/>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={positions.length / 3}
array={positions}
itemSize={3}
/>
<bufferAttribute
attach="attributes-uv"
count={uvs.length / 2}
array={uvs}
itemSize={2}
/>
</bufferGeometry>
</mesh>,
scene
)}
<points ref={points}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={particlesPosition.length / 3}
array={particlesPosition}
itemSize={3}
/>
<bufferAttribute
attach="attributes-color"
count={colors.length / 3}
array={colors}
itemSize={3}
/>
<bufferAttribute
attach="attributes-aScale"
count={scales.length}
array={scales}
itemSize={1}
/>
</bufferGeometry>
<shaderMaterial
blending={AdditiveBlending}
depthWrite={false}
// transparent={true}
fragmentShader={fragmentShader}
vertexShader={vertexShader}
uniforms={uniforms}
/>
</points>
</>
);
};
export default FboParticles;
You just need use states, if you want re-render your component use states like this:
const App = () => {
const [,forceUpdate] = useState({update: true});
return (
<button onClick{() => forceUpdate({update:true})}>re render this component</button>
)
}
use this forceUpdate anywhere, but focus it should take object if you want to write int on that you should set different value so yes use random int on that
forceUpdate(Math.random())

How to convert moment.js to date-fns

I am trying to use the code from this codepen in my react/date-fns app.
import {useEffect, useRef, useState} from 'react'
import {add, sub, format, parse} from 'date-fns'
const timeRegExp = /([01][0-9]|2[0-3])[0-5][0-9]/
const Time = () => {
const refs = useRef([])
const [values, setValues] = useState([])
useEffect(() => {
refs.current.forEach((input, i) => {
input.addEventListener('keydown', (e: any) => {
if (e.keyCode === 37) {// left arrow
if (i !== 0) refs.current[i - 1].focus()
} else if (e.keyCode === 39) {// right arrow
if (i !== 3) refs.current[i + 1].focus()
} else if (/48|49|50|51|52|53|54|55|56|57/.test(e.keyCode)) {// 0 ~ 9
const time = [0, 1, 2, 3].map(i => {
return i === i ? String.fromCharCode(e.keyCode) : refs.current[i].value
}).join('')
if (timeRegExp.test(time)) {
refs.current[i].value = String.fromCharCode(e.keyCode)
if (i !== 3) refs.current[i + 1].focus()
}
} else if (e.keyCode === 8) {// delete / backspace
refs.current[i].value = 0
if (i !== 0) refs.current[i - 1].focus()
} else if (e.keyCode === 38) {// up arrow
// if (i === 0 && refs.current[0].value === '2') {
//
// } else {
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').add(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
// }
} else if (e.keyCode === 40) {// down arrow
// if (i === 0 && refs.current[0].value === '0') {
// } else {
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').subtract(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
// }
}
e.preventDefault()
})
})
// input.addEventListener('keydown', e => {}))
}, [])
return <div>
<input ref={e => refs.current[0] = e} defaultValue={0}/>
<input ref={e => refs.current[1] = e} defaultValue={0}/>
<span>:</span>
<input ref={e => refs.current[2] = e} defaultValue={0}/>
<input ref={e => refs.current[3] = e} defaultValue={0}/>
</div>
}
export default Time
How can I convert commented out code to use date-fns?
You'll have to do the porting work, there's no universal way around it. Although it's reasonably easy. For instance, for the part of the code
// let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
// time = moment(time, 'HHmm').add(i % 2 ? 1 : 10, Math.floor(i / 2) ? 'm' : 'h').format('HHmm').split('');
// [0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
it would be
let time = [0, 1, 2, 3].map(i => refs.current[i].value).join('')
const parsed = parse(time, "HHmm");
console.log("parsed", parsed)
const addF = Math.floor(i / 2) ? addMinutes : addHours;
const added = addF(parsed, i % 2 ? 1 : 10);
const formatted = format(added, "HHmm");
time = formatted.split('');
[0, 1, 2, 3].forEach(i => refs.current[i].value = time[i])
code is split into assignments for readability, but you could just chain the functions with lodash compose + date-fns/fp if you'd prefer oneliners.

useState not setting value

I am trying to make a calculator, but when i click the number buttons, the screen doesnt update. I checked the handler, and it seems like it should work. I also logged the calc(see code for reference) and it is not updating. What is wrong?
import React, {useState} from 'react';
import Screen from './screen.js'
import Button from './button.js'
import ButtonBox from './buttonBox.js'
const btnValues = [
["C", "+-", "%", "/"],
[7, 8, 9, "X"],
[4, 5, 6, "-"],
[1, 2, 3, "+"],
[0, ".", "="],
];
const CalcBody =() => {
{/*res as in result */}
let [calc, setCalc] = useState({
sign: "",
num: 0,
res: 0,
});
const numClickHandler = (e) => {
e.preventDefault();
const value = e.target.innerHTML;
if (calc.num.length < 16) {
setCalc({
...calc,
num:
calc.num === 0 && value === "0"
? "0"
: calc.num % 1 === 0
? Number(calc.num + value)
: calc.num + value,
res: !calc.sign ? 0 : calc.res,
});
}
console.log(calc.num)
};
return (
<div className="wrapper">
<Screen value={calc.num ? calc.num : calc.res} />
<ButtonBox>
{btnValues.flat().map((btn, i) => {
return (
<Button
key={i}
className={btn === "=" ? "equals" : ""}
value={btn}
onClick={numClickHandler
}
/>
);
})}
</ButtonBox>
</div>
);
}
export default CalcBody;

Animating bufferAttribute with react-three-fiber and react-spring

I have simple Points mesh with custom shader and buffer geometry.
The geometry has position, size and color attributes.
On pointer hover, the hovered vertex turns into red color.
So far so good.
Now I would like to animate the change of color of the hovered vertex.
Here is the code snippet for the Points mesh:
const Points = (
props = { hoveredIndex: null, initialCameraZ: 0, array: [], sizes: [] }
) => {
const [_, setHover] = useState(false);
const vertices = new Float32Array(props.array);
const sizes = new Float32Array(props.sizes);
const _colors = genColors(props.sizes.length); // returns [] of numbers.
const uniforms = useMemo(() => {
return {
time: { type: "f", value: 0.0 },
cameraZ: { type: "f", value: props.initialCameraZ }
};
}, [props.initialCameraZ]);
// trying to use react-spring here
const [animProps, setAnimProps] = useSpring(() => ({
colors: _colors
}));
const geometry = useUpdate(
(geo) => {
if (props.hoveredIndex !== null) {
const i = props.hoveredIndex * 3;
const cols = [..._colors];
cols[i] = 1.0;
cols[i + 1] = 0.0;
cols[i + 2] = 0.0;
geo.setAttribute(
"color",
new THREE.BufferAttribute(new Float32Array(cols), 3)
);
setAnimProps({ colors: cols });
} else {
geo.setAttribute(
"color",
new THREE.BufferAttribute(new Float32Array(_colors), 3)
);
setAnimProps({ colors: _colors });
}
},
[props.hoveredIndex]
);
return (
<a.points
onPointerOver={(e) => setHover(true)}
onPointerOut={(e) => setHover(false)}
>
<a.bufferGeometry attach="geometry" ref={geometry}>
<bufferAttribute
attachObject={["attributes", "position"]}
count={vertices.length / 3}
array={vertices}
itemSize={3}
/>
<bufferAttribute
attachObject={["attributes", "size"]}
count={sizes.length}
array={sizes}
itemSize={1}
/>
<a.bufferAttribute
attachObject={["attributes", "color"]}
count={_colors.length}
array={new Float32Array(_colors)}
// array={animProps.colors} // this does not work
itemSize={3}
/>
</a.bufferGeometry>
<shaderMaterial
attach="material"
uniforms={uniforms}
vertexShader={PointsShader.vertexShader}
fragmentShader={PointsShader.fragmentShader}
vertexColors={true}
/>
</a.points>
);
};
Full code and example is available on codesandbox
When I try to use animProps.colors for color in bufferAttribute it fails to change the color.
What am i doing wrong? How to make it right?
I know I could create start and target color attributes, pass them to the shader and interpolate there but that would beat the purpose of using react-three-fiber.
Is there a way animating buffer attributes in react-three-fiber?
This likely isn't working as expected because changes made to buffer attributes need to be explicitly flagged for sending to the GPU. I suppose that react-spring does not do this out of the box.
Here's a vanilla example, note the usage of needsUpdate:
import { useFrame } from '#react-three/fiber'
import React, { useRef } from 'react'
import { BufferAttribute, DoubleSide } from 'three'
const positions = new Float32Array([
1, 0, 0,
0, 1, 0,
-1, 0, 0,
0, -1, 0
])
const indices = new Uint16Array([
0, 1, 3,
2, 3, 1,
])
const Comp = () => {
const positionsRef = useRef<BufferAttribute>(null)
useFrame(() => {
const x = 1 + Math.sin(performance.now() * 0.01) * 0.5
positionsRef.current.array[0] = x
positionsRef.current.needsUpdate = true
})
return <mesh>
<bufferGeometry>
<bufferAttribute
ref={positionsRef}
attach='attributes-position'
array={positions}
count={positions.length / 3}
itemSize={3}
/>
<bufferAttribute
attach="index"
array={indices}
count={indices.length}
itemSize={1}
/>
</bufferGeometry>
<meshBasicMaterial
color={[0, 1, 1]}
side={DoubleSide}
/>
</mesh>
}
For further reading check out this tutorial.

How to prevent re-rendering with callbacks as props in ReactJS?

I'm practicing with the new hooks functionnality in ReactJS by refactoring the code from this tutorial with TypeScript.
I am passing a callback from a parent component to a child component threw props that has to be executed on a click button event.
My problem is this: I have an alert dialog that appears twice instead of once when the game has been won.
I assumed this was caused by a component re-rendering so I used the useCallback hook to memoize the handleClickOnSquare callback. The thing is, the alert dialog still appears twice.
I guess I'm missing something that has a relation with re-rendering, does someone have an idea what might cause this behavior ?
Here is my code:
Game.tsx
import React, { useState, useCallback } from 'react';
import './Game.css';
interface SquareProps {
onClickOnSquare: HandleClickOnSquare
value: string;
index: number;
}
const Square: React.FC<SquareProps> = (props) => {
return (
<button
className="square"
onClick={() => props.onClickOnSquare(props.index)}
>
{props.value}
</button>
);
};
interface BoardProps {
squares: Array<string>;
onClickOnSquare: HandleClickOnSquare
}
const Board: React.FC<BoardProps> = (props) => {
function renderSquare(i: number) {
return (
<Square
value={props.squares[i]}
onClickOnSquare={props.onClickOnSquare}
index={i}
/>);
}
return (
<div>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
);
};
export const Game: React.FC = () => {
const [history, setHistory] = useState<GameHistory>(
[
{
squares: Array(9).fill(null),
}
]
);
const [stepNumber, setStepNumber] = useState(0);
const [xIsNext, setXIsNext] = useState(true);
const handleClickOnSquare = useCallback((index: number) => {
const tmpHistory = history.slice(0, stepNumber + 1);
const current = tmpHistory[tmpHistory.length - 1];
const squares = current.squares.slice();
// Ignore click if has won or square is already filled
if (calculateWinner(squares) || squares[index]) return;
squares[index] = xIsNext ? 'X' : 'O';
setHistory(tmpHistory.concat(
[{
squares: squares,
}]
));
setStepNumber(tmpHistory.length);
setXIsNext(!xIsNext);
}, [history, stepNumber, xIsNext]);
const jumpTo = useCallback((step: number) => {
setHistory(
history.slice(0, step + 1)
);
setStepNumber(step);
setXIsNext((step % 2) === 0);
}, [history]);
const current = history[stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go back to move n°' + move :
'Go back to the start of the party';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
let status: string;
if (winner) {
status = winner + ' won !';
alert(status);
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClickOnSquare={handleClickOnSquare}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
function calculateWinner(squares: Array<string>): string | null {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
types.d.ts
type GameHistory = Array<{
squares: Array<string>
}>;
type HandleClickOnSquare = (index: number) => void;
Thanks
Your code is too long to find the cause of extra rerender. Note that React might need an extra render.
To avoid an extra alert use useEffect:
let status: string;
if (winner) {
status = winner + ' won !';
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
useEffect(() => {
if (winner) alert(status)
}, [winner]);

Resources