How do I get a component to "listen" to changes in global state in a sibling component?

I’m building a wordle clone. I’ve structured it so that the keypad and the letter display grid are two separate components, Keypad.js and Row,js respectively. Project structure is as follows:

src
-components
 |-Grid.js
 |-Keypad.js
 |-Row.js
-App.js
-AppContex.js
-index.js

When a user enters a letter on the keypad, initially I want that letter to appear in the first index of the first row, as per the game of wordle. How do I get Row.js to "listen" to changes in Keypad.js, so that when a user enters a letter, it shows up in the corresponding index in the grid row?

My approach so far has been to create global state using the Context API, where I’ve made an empty grid to share to the entire app:

AppContext.js

import { createContext } from "react";

const guessRows = [
    ['', '', '', '', ''],
    ['', '', '', '', ''],
    ['', '', '', '', ''],
    ['', '', '', '', ''],
    ['', '', '', '', ''],
    ['', '', '', '', '']
]

export const AppContext = createContext()

const AppContextProvider = (props) => {
    return(
        <AppContext.Provider value = {guessRows}>
            {props.children}
        </AppContext.Provider>
    )
}

export default AppContextProvider

In Keypad.js, the letter the user enters is used to update the context (or at least that’s what I think it’s doing):

import { useContext,useState } from "react"
import { AppContext } from "../AppContext"

const Keypad = () => {
  const guessRows = useContext(AppContext);
  let currentRow = 0;
  let currentTile = 0;

  const letters = [
    "Q",
    "W",
    "E",
    "R",
   // etc
  ];

  const handleClick = (letter) => {
    guessRows[currentRow][currentTile] = letter;
    currentTile++;
    console.log("guess rows", guessRows);
  };

  return (
    <div className="keyboard-container">
      {letters.map((letter, index) => {
        return (
          <div className="key" key={index} onClick={() => handleClick(letter)}>
            {letter}
          </div>
        );
      })}
    </div>
  );
};
 
export default Keypad;

…then in Row.js, I’m looping through the context and rendering the rows:

Row.js

import { useContext } from "react";
import { AppContext } from "../AppContext";


const Row = () => {

  const rowData = useContext(AppContext)
  const currentRow = rowData[0]

  return (
    <div className="row">
      {currentRow.map((letter,index) => {
        return(
          <div className="tile" id = {index}>{letter}</div>
        )
      })}
    </div>
  )
}
 
export default Row;

Unsurprisingly this isn’t working, so any suggestions would be appreciated.

>Solution :

Your guessRows should be put into the context state, so that it can have a state setter passed down – then instead of doing guessRows[currentRow][currentTile] = letter;, call the state setter. Similarly, currentTile++; should be replaced with a state update, since this is React – the view should flow from the state.

const AppContextProvider = (props) => {
    const [guessRows, setGuessRows] = useState([
        ['', '', '', '', ''],
        ['', '', '', '', ''],
        ['', '', '', '', ''],
        ['', '', '', '', ''],
        ['', '', '', '', ''],
        ['', '', '', '', '']
    ]);
    return(
        <AppContext.Provider value = {{ guessRows, setGuessRows }}>
            {props.children}
        </AppContext.Provider>
    );
};
const { guessRows, setGuessRows } = useContext(AppContext);
const [currentRow, setCurrentRow] = useState(0);
const [currentTile, setCurrentTile] = useState(0);

const handleClick = (letter) => {
    setGuessRows(
        guessRows.map((row, i) => i !== currentRow ? row : (
            row.map((item, j) => j === currentTile ? letter : item)
        ))
    );
    setCurrentTile(currentTile + 1);
};

And then when the state setter is called, the components will re-render, including Row, which will show the changes made.

Leave a Reply