Missing Document Title
Theme: Next, 1
React State Flow
Review our Tic Tac Toe Game
import React, { useState } from 'react'type Square = 'X' | 'O' | ' 'type Row = [Square, Square, Square]type Board = [Row, Row, Row]type Game = {board: Boardid: null | numberwinner: null | string}
function App() {const [game, setGame] = useState<Game>({board: [[' ', ' ', ' '],[' ', ' ', ' '],[' ', ' ', ' '],],id: null,winner: null,})
async function handleClickCell(row: number, column: number) {if (// No game idgame.id === undefined ||// A winner existsgame.winner ||// The space isn't blankgame.board[row][column] !== ' ') {return}// Generate the URL we needconst url = `https://sdg-tic-tac-toe-api.herokuapp.com/game/${game.id}`// Make an object to send as JSONconst body = { row: row, column: column }
// Make a POST request to make a moveconst response = await fetch(url, {method: 'POST',headers: { 'content-type': 'application/json' },body: JSON.stringify(body),})if (response.ok) {console.log('x')// Get the response as JSONconst newGame = (await response.json()) as Game// Make that the new state!setGame(newGame)}}
async function handleNewGame() {// Make a POST request to ask for a new gameconst response = await fetch('https://sdg-tic-tac-toe-api.herokuapp.com/game',{method: 'POST',headers: { 'content-type': 'application/json' },})if (response.ok) {// Get the response as JSONconst newGame = (await response.json()) as Game// Make that the new state!setGame(newGame)}}
const header = game.winner ? `${game.winner} is the winner` : 'Tic Tac Toe'
return (<div><h1>{header} - <button onClick={handleNewGame}>New</button></h1><ul>{game.board.map((boardRow, rowIndex) => {return boardRow.map((cell, columnIndex) => {return (<likey={columnIndex}className={cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(rowIndex, columnIndex)}>{cell}</li>)})})}</ul></div>)
Let us extract a component for each cell!
Define the component right in the App.jsx
- Copy/paste the implementation of a specific
li
- What else is needed?
export function Cell() {return (<liclassName={cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(rowIndex, columnIndex)}>{cell}</li>)}
Needed
- rowIndex
- columnIndex
- cell
- something to handle clicking the cell
Wouldn't it be nice to be able to use the parent's state!?
- Well, you cannot...
We can pass down the parts of state we need.
- Pass down the cell's value
- Pass down the row
- Pass down the column
type CellProps = {rowIndex: numbercolumnIndex: numbercell: string}export function Cell(props: CellProps) {return (<liclassName={props.cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(props.rowIndex, props.columnIndex)}>{props.cell}</li>)}
Define a local click handler
type CellProps = {rowIndex: numbercolumnIndex: numbercell: string}export function Cell(props: CellProps) {function handleClickCell() {console.log(`You clicked on ${props.rowIndex} and ${props.columnIndex}`)}return (<liclassName={props.cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(props.rowIndex, props.columnIndex)}>{props.cell}</li>)}
This is already better!
<ul>{game.board.map((boardRow, rowIndex) => {return boardRow.map((cell, columnIndex) => {return (<Cellkey={columnIndex}cell={cell}rowIndex={rowIndex}columnIndex={columnIndex}/>)})})}</ul>
But how do we dispatch the API and update the state?
- The
cell
is a read-onlyprop
in theCell
- If we did call the API in the cell, how can we transport the state to the parent?
- Whatever to do?
State down
- We are sending the state DOWN by doing something like
cell={cell}
- This sends the PARENT's state to the CHILD as
props
Events up
- We still have
handleClickCell
in the parent. handleClickCell
does what we need.- Rename it to
recordMove
- And pass the event handling function down to the child component
<Cellkey={columnIndex}cell={cell}rowIndex={rowIndex}columnIndex={columnIndex}recordMove={recordMove}/>
The Cell component can now call UP to the parent's recordMove
- This is sending the event (something happened) to the parent
type CellProps = {rowIndex: numbercolumnIndex: numbercell: stringrecordMove: (rowIndex: number, columnIndex: number) => void}function handleClickCell() {// Send the event UPwards by calling the `recordMove` function we were givenprops.recordMove(props.rowIndex, props.columnIndex)}
Conceptual Model - State Down
+----------------+ +--------------+| App | | Cell || | | || State: | | Props: || board[r][c] -----------------> cell || row | | row || column | | column || | +-----------> recordMove || Functions: | | | || recordMove --------+ +--------------+| |+----------------+
Conceptual Model - Events Up
[.column]
CellCell Receives ClickCalls local onClickCalls props.recordMovewhich is a function.But the function is *FROM*the App, so that is thecontext of where run
[.column]
recordMove runsin the App componentCalls the APIupdates stateReact sees that stateis updated and re-rendersReact makes new `Cell`components to replacethe old onesThere are new valuesfor `props.cell`so the UI draws the*current* game.
Code is more DRY
- There is one place for each concept
[.column]
The
App
- Deals with the API
- Manages the state
- Renders the board
- Tells the Cell where it lives and what to do when clicked
[.column]
- The
Cell
- Draws its own UI (a
li
) - Knows its row and column
- Knows its value and nothing else it needs
- Handles a click
- Calls the parent when clicked
- Draws its own UI (a
Separation of Concerns
- The
Cell
knows only what it needs - The
App
does not know or care how theCell
renders or handles clicks
Missing a Concern?
Maybe we need a
Game
component?Could move the state and the
ul
rendering there.Where would the
New Game
button go?- Likely move into the
Game
component and user interface
- Likely move into the
Progress!
Extract that Cell
component to a new file
Explore some cleanup using TypeScript syntax sugar
- Object shortcut
const body = { row: row, column: column }
- The key name
row
is the same as the name of the variable holding the valuerow
- Shortcut (structuring the object):
const body = { row, column }
Flip side.
De-structuring the object
props.rowIndex
, props.columnIndex
, props.cell
, props.recordMove
, ...
Destructuring
- If we have an object like
const person = {name: 'Susan',favoriteColor: 'green'salary: 1000000}
we can make local variables name
, favoriteColor
, and salary
and initialize their values from the object
const { name, favoriteColor, salary } = person
const { name, favoriteColor, salary } = person// ^ ^ ^ v// | | | |// | | | |// ^--------^-------------^---------<
Notice the { }
braces are on the left, and the object is on the right
Back to our Cell
type CellProps = {rowIndex: numbercolumnIndex: numbercell: string}export function Cell(props) {function handleClickCell() {// Send the event UPwards by calling the `recordMove` function we were givenprops.recordMove(props.rowIndex, props.columnIndex)}return (<liclassName={props.cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(props.rowIndex, props.columnIndex)}>{props.cell}</li>)}
Destructuring props
at the top of a function
type CellProps = {rowIndex: numbercolumnIndex: numbercell: string}export function Cell(props) {const { rowIndex, columnIndex, cell, recordMove} = propsfunction handleClickCell() {// Send the event UPwards by calling the `recordMove` function we were givenrecordMove(rowIndex, columnIndex)}return (<liclassName={cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(rowIndex, columnIndex)}>{cell}</li>)}
We can destructure the props right in the function declaration.
type CellProps = {rowIndex: numbercolumnIndex: numbercell: string}export function Cell({ rowIndex, columnIndex, cell, recordMove}) {function handleClickCell() {// Send the event UPwards by calling the `recordMove` function we were givenrecordMove(rowIndex, columnIndex)}return (<liclassName={cell === ' ' ? '' : 'taken'}onClick={() => handleClickCell(rowIndex, columnIndex)}>{cell}</li>)}
- ... makes it feel like the properties are nice local variables.
- ... and that syntax is sometimes more straightforward and tidier.