0

I'm building a react app which solves a sudoku board and visualises the solving algorithm.

I'm trying to convert the class components to functional components, but am not sure how to get the state changes working.

The problematic code is:

  async solve() {
    await this.sleep(0);
    const board = this.state.board;
    for (let row = 0; row < 9; row++) {
      for (let col = 0; col < 9; col++) {
        if (board[row][col] !== 0) {
          // can't change filled cells
          continue;
        }
        // try 1 to 9 for the empty cell
        for (let num = 1; num <= 9; num++) {
          if (App.isValidMove(board, row, col, num)) {
            // valid move, try it
            board[row][col] = num;
            this.setState({ board: board });
            if (await this.solve()) {
              return true;
            } else {
              // didn't solve with this move, backtrack
              board[row][col] = 0;
              this.setState({ board: board });
            }
          }
        }
        // nothing worked for empty cell, must have deprived boards solution with a previous move
        return false;
      }
    }
    // all cells filled
    return true;
  }

(Full code here and the app is hosted here)

Here I need async so I can use sleep() to visualise the algorithm. I use this.setState({ board: board }); to trigger a re-render everytime the board is mutated.


When trying to convert to a functional component I tried:

  • useState hook, I used const [board, setBoard] = useState(initialBoard); and replaced the this.setState calls with setBoard(board). This didn't work because the hook is being called in a loop
  • wrapping useState with useEffect (i.e. useEffect(() => setBoard(board), [board])). This didn't compile, gets the error React Hook "useEffect" may be executed more than once...
  • also looked into useReducer but have read it doesn't work well with async

My questions are:

  • Is this even possible to convert to a functional component?
  • If yes, what hook should I use and would it require a redesign of my solve() function?
  • Is it even best to convert this from a class component to a functional component?
wilmol
  • 1,429
  • 16
  • 22
  • Hey have you tried `useAsyncEffect` instead of useEffect? I use this npm package whenever I have asynchronous hooks like this [npm link](https://www.npmjs.com/package/use-async-effect) – mcmohorn May 01 '20 at 12:56

2 Answers2

1

You can use an effect every time the board changes this effect will run. When there are no more moves then the board doesn't change and the effect won't run anymore.

Here is an example of how you can do this:

const { useState, useEffect } = React;
const getCell = (board) => {
  const row = board.findIndex((row) =>
    row.some((cell) => !cell.checked)
  );
  if (row === -1) {
    return [-1, -1, false];
  }
  const col = board[row].findIndex((cell) => !cell.checked);
  return [row, col, true];
};
const initialState = [
  [{ checked: false }, { checked: false }],
  [{ checked: false }, { checked: false }],
];
const App = () => {
  const [board, setBoard] = useState(initialState);
  useEffect(() => {
    const timer = setTimeout(
      () =>
        setBoard((board) => {
          const [row, col, set] = getCell(board);
          if (set) {//do we need to set board (there are moves)
            const boardCopy = [...board];
            boardCopy[row] = [...board[row]];
            boardCopy[row][col] = {
              ...boardCopy[row][col],
              checked: true,
            };
            return boardCopy;
          }
          //this will return the same as passed in
          //  so App will not re render because board
          //  will not have changed
          return board;//no moves left because set is false
        }),
      1000
    );
    return () => clearTimeout(timer);
  }, [board]);
  return (
    <div>
      <button onClick={() => setBoard(initialState)}>
        reset
      </button>
      <table>
        <tbody>
          {board.map((row, i) => (
            <tr key={i}>
              {row.map(({ checked }, i) => (
                <td key={i}>{checked ? 'X' : 'O'}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
0

Got it working with useState hook.

The issue was I wasn't copying the board correctly; needed to use const boardCopy = [...board]; (rather than const boardCopy = board).

Full code of the functional component is here.

Previous class component code is here.

wilmol
  • 1,429
  • 16
  • 22