Home > Enterprise >  How to rerender a component every time a value changes
How to rerender a component every time a value changes

Time:11-05

I'm making a sudoku solver, and I have the actual solver working. However, I want the sudoku solver to be sort of 'animated' and every time one of the tiles changes I want that change to be shown. With the way I have it written currently it will only re render once the whole sudoku is solved.

Here is my code:


import { useEffect, useState } from 'react'
import './SudokuSolver.css'

export default function SudokuSolver() {
  //0 stands for empty
  const sudoku = [8, 0, 0, 0, 0, 0, 0, 0, 0, 
                  0, 0, 3, 6, 0, 0, 0, 0, 0, 
                  0, 7, 0, 0, 9, 0, 2, 0, 0,
                  0, 5, 0, 0, 0, 7, 0, 0, 0,
                  0, 0, 0, 0, 4, 5, 7, 0, 0,
                  0, 0, 0, 1, 0, 0, 0, 3, 0,
                  0, 0, 1, 0, 0, 0, 0, 6, 8,
                  0, 0, 8, 5, 0, 0, 0, 1, 0,
                  0, 9, 0, 0, 0, 0, 4, 0, 0]
  const [sudokuObject, setSudokuObject] = useState(sudoku.map(tile => {
    return {
      value: tile,
      predetermined: tile !== 0 ? true : false
    }
  }))
  const sudokuSize = Math.sqrt(sudoku.length)
  const tileSizeInPixels = 70;
  
  const pixelsFromTop = (index) => {
    const subsectionBuffer = Math.floor((index / sudokuSize) / Math.sqrt(sudokuSize)) * 20
    return ((Math.floor(index / sudokuSize)) * tileSizeInPixels)   subsectionBuffer
  }

  const pixelsFromLeft = (index) => {
    const subsectionBuffer = Math.floor((index % sudokuSize) / 3) * 20; 
    return ((index % sudokuSize) * tileSizeInPixels)   subsectionBuffer
  }

  const isLegalByRow = (testNum, index, sudokuObject) => {
    //get the row number (possible values: 0 through sudokuSize)
    const rowNum = Math.floor(index / sudokuSize)
    for(let i = 0; i < sudokuSize; i  ){
      if(sudokuObject[i   (rowNum * sudokuSize)].value === testNum)
        return false
    }
    return true
  }

  const isLegalByColumn = (testNum, index, sudokuObject) => {
    //get the column number (possible values: 0 through sudokuSize)
    const columnNum = (index) % (sudokuSize)
    for(let i = 0; i < sudokuSize; i  ){
      if(sudokuObject[(i * sudokuSize)   columnNum].value === testNum)
        return false;
    }
    return true;
  }

  const isLegalBySubsection = (testNum, index, sudokuObject) => {
    const subsectionSize = Math.sqrt(sudokuSize)
    const rowNum = Math.floor(index / sudokuSize)
    const columnNum = index % sudokuSize 
    //gets 'subsection indexes', for instance in a 9x9 grid there's 9 subsections (3x3) boxes
    //this determines which box to check
    const subsectionRow = Math.floor(rowNum / Math.sqrt(sudokuSize))
    const subsectionColumn = Math.floor(columnNum / Math.sqrt(sudokuSize))
    //example: box [1, 1] should check indexes: 30, 31, 32, 39, 40, 41, 48, 49, 50
    const subsectionRowSize = subsectionSize * sudokuSize
    const startingIndex = subsectionRowSize * subsectionRow   (subsectionColumn * subsectionSize)
    for(let i = 0; i < subsectionSize; i  ){
      for(let j = 0; j < subsectionSize; j  ){
        if(sudokuObject[(startingIndex   j)   i * sudokuSize].value === testNum)
          return false
      }
    }
    return true
  }
  
  const isLegal = (testNum, index, tempObject) => {
    return(isLegalBySubsection(testNum, index, tempObject) && 
    isLegalByRow(testNum, index, tempObject) && isLegalByColumn(testNum, index, tempObject))
  }
 const autoSolveSudoku = () => {
    /* the algorithm: 
      iterate through each tile of the sudoku, for each tile:
        starting at the number 1, try every possible number (up to 9 for a standard sudoku)
        stop at a number when it's legal and go forward to the next tile
        if(none of the possible numbers are legal):
          go back to previous tiles(again skip if predetermined),
          on previous tiles: 
           start at the number that was left there and go up to sudokuSize
           once a legal number is found start going forward again
           if no legal number is found keep going backward
    */
    let goingForward = true;
    let tempObject = [...sudokuObject];
    for(let i = 0; i < tempObject.length; i  ){
      if(tempObject[i].predetermined){
        if(goingForward)
          continue;
        i -=2;
        continue;
      }
      for(let j = tempObject[i].value; j <= sudokuSize; j  ){
        if(j === 0){
          continue;
        }
        if(!goingForward && tempObject[i].value === sudokuSize){
          tempObject[i].value = 0;
          break;
        }
        if(isLegal(j, i, tempObject)){
          tempObject[i].value = j;
          goingForward = true;
          break;
        }
        if(j === sudokuSize){
          tempObject[i].value = 0;
          goingForward = false;
        }
       
      }
      if(!goingForward)
        i -= 2
        setSudokuObject(tempObject)
    }
    
  }

  return <div style={{top:'50px', left:'50px', position:'relative'}}>
    {sudokuObject.map((tile, index) => {
      return <div id="tile" style={{top: pixelsFromTop(index), left: pixelsFromLeft(index), color: tile.predetermined && 'blueviolet'}}>
        
        {index - 1 % 9 === 0 && index !== 1 && index !== 0 && <br/>}
        {tile.value !== 0 ? tile.value : " "}
      </div> 
    })}
    <button id='solvebutton' onClick={autoSolveSudoku}>Auto Solve</button>

  </div>
}


I know setSudokuObject() would only be called once the autoSolveSudoku() function finishes, so is there a way to update it every time a value changes? Honestly I've never encountered this before so I'm not sure how to approach it.

CodePudding user response:

You can use useEffect hook in ReactJS.

This is an example:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  },[count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count   1)}>
        Click me
      </button>
    </div>
  );
}

In the code above, each time that the value in the counter changes, the things which are in the useEffect hook will execute again.

So, in general this is how you can use it:

useEffect(()=>{
    // Codes you want to execute each time that variableYouWantToControl changes. 
},[variableYouWantToControl]

Note

The variable that want to control it (in the example above: counter), should be of the type useState. So, if you define a variable like let counter = 0, it won't work. It should be defined like const [counter, setCounter] = useState(0) and then you should the value of counter by: setCounter(1).

CodePudding user response:

For this to work, you probably want to turn your sudoku solver into a resumable function that solves exactly one step, calls setSudokuObject with the new result, and then terminates to be called again once the button is clicked repeatedly. In the meantime, React will re-render your component.

To do that, the variables relevant for the outer loop could be turned into React states:

  const [lastLocation, setLastLocation] = useState(0);
  const [goingForward, setGoingForward] = useState(true);

Afterwards, they can inform the algorithm of the previous state:

for (let i = lastLocation; i < tempObject.length; i  ) {

If the move is legal, the solveStep function updates the grid and returns:

        if (isLegal(j, i, tempObject)) {
          tempObject[i].value = j;
          setSudokuObject(tempObject);
          setLastLocation(i);
          setGoingForward(true);
          return;
        }
        if (j === sudokuSize) {
          tempObject[i].value = 0;
          // Here, I am not sure whether the `goingForward` variable 
          // will be updated for the current loop correctly. Experiment! :)
          setGoingForward(false);
        }

I'll leave it as an exercise to the reader to implement buttons for "Solve 10 steps" and "Solve 50 steps".

  • Related