javascriptcssreactjsreact-hookscss-transitions

ReactJS problem with changing direction of translateX() transformation


Given :

function App() {        
        const [xPos, setXPos ] = React.useState(0);
    const [style, setStyle] = React.useState({transform: `translateX(${xPos}px)`});
  
    const onClick =(direction) => {
       (direction === "left") ? setXPos(x => x-100) : setXPos(x => x +100);
       setStyle({transform: `translateX(${xPos}px)`});
           console.log(xPos)
        }

    return (
          <div className="main_container">
             <button className="left_button" onClick={() => onClick("left")}>slide left</button>
             <div className="forecast_slider" >
                <div className="forecast_container" style={style} > 
                   {forecastBuilder()}
                </div>
             </div>
             <button className="right_button" onClick={() => onClick("right")}>slide right</button>
          </div>
       )
   }

const forecastBuilder = () => {
const cell = [];
  for(var i = 1 ; i < 8 ; i++){
    cell.push(
        <div className={i}>
        {i}
        <img src="https://imgs.michaels.com/MAM/assets/1/5E3C12034D34434F8A9BAAFDDF0F8E1B/img/0E9397ED92304202B4A25D7387A74515/M10118706_2.jpg" width="100" height="80" border="1px solid black" />
        <br></br>
        <span>day {i}</span>
      </div>
    )
  }
  return cell;
}


ReactDOM.render(<App />, document.querySelector("#app"));
.main_container {
  display:flex;
}
.forecast_container { 
    display: flex;
    width: 510px;
    height: 130px;
    
    margin-left: auto;
    margin-right: auto;

    align-items: center;
    text-align: center;
    
    transition: transform 250ms;
  }
  .forecast_slider {
    background-color: black;
    color: white;
  
     overflow:hidden;
    float:right;
  }
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
 <div id="app"></div>

with JSFiddle link here ,

I want to make the translateX() animation increment and decrement normally. Currently, I have to click twice a button to change the direction. I have no clue why this is happening. I tried setting the initial style's transform parameter to 0px. I haven't tried other things since honestly, I am short of ideas, this bug is beyond my understanding of React.

Does anyone have any idea how I could solve this?


Solution

  • The problem is you try to use the updated xPos state in your onClick handler right after you "updated" it: setStyle({transform: `translateX(${xPos}px)`})

    Don't forget that useState is asynchronous just like setState in class components. You can't update the state on one line and assume it's already changed on the next one. You'll likely use the unchanged state.

    Create a new variable and update both states using that one:

    function App() {
      const [xPos, setXPos] = React.useState(0);
      const [style, setStyle] = React.useState({
        transform: `translateX(${xPos}px)`,
      });
    
      const onClick = (direction) => {
        let x = direction === 'left' ? xPos - 100 : xPos + 100
        setXPos(x)
        setStyle({ transform: `translateX(${x}px)` });
        console.log(xPos);
      };
    
      return (
        <div className="main_container">
          <button className="left_button" onClick={() => onClick('left')}>
            slide left
          </button>
          <div className="forecast_slider">
            <div className="forecast_container" style={style}>
              {forecastBuilder()}
            </div>
          </div>
          <button className="right_button" onClick={() => onClick('right')}>
            slide right
          </button>
        </div>
      );
    }
    
    const forecastBuilder = () => {
      const cell = [];
      for (var i = 1; i < 8; i++) {
        cell.push(
          <div className={i}>
            {i}
            <img
              src="https://imgs.michaels.com/MAM/assets/1/5E3C12034D34434F8A9BAAFDDF0F8E1B/img/0E9397ED92304202B4A25D7387A74515/M10118706_2.jpg"
              width="100"
              height="80"
              border="1px solid black"
            />
            <br></br>
            <span>day {i}</span>
          </div>
        );
      }
      return cell;
    };
    
    ReactDOM.render(<App />, document.querySelector('#app'));
    .main_container {
      display: flex;
    }
    
    .forecast_container {
      display: flex;
      width: 510px;
      height: 130px;
      margin-left: auto;
      margin-right: auto;
      align-items: center;
      text-align: center;
      transition: transform 250ms;
    }
    
    .forecast_slider {
      background-color: black;
      color: white;
      overflow: hidden;
      float: right;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="app"></div>

    Also, you could simply get rid of the useStyles hook as it's just some extra fluff around xPos:

    function App() {
      const [xPos, setXPos] = React.useState(0);
    
      return (
        <div className="main_container">
          <button className="left_button" onClick={() => setXPos(xPos - 100)}>
            slide left
          </button>
          <div className="forecast_slider">
            <div className="forecast_container" style={{ transform: `translateX(${xPos}px)` }}>
              {forecastBuilder()}
            </div>
          </div>
          <button className="right_button" onClick={() => setXPos(xPos + 100)}>
            slide right
          </button>
        </div>
      );
    }
    
    const forecastBuilder = () => {
      const cell = [];
      for (var i = 1; i < 8; i++) {
        cell.push(
          <div className={i}>
            {i}
            <img
              src="https://imgs.michaels.com/MAM/assets/1/5E3C12034D34434F8A9BAAFDDF0F8E1B/img/0E9397ED92304202B4A25D7387A74515/M10118706_2.jpg"
              width="100"
              height="80"
              border="1px solid black"
            />
            <br></br>
            <span>day {i}</span>
          </div>
        );
      }
      return cell;
    };
    
    ReactDOM.render(<App />, document.querySelector('#app'));
    .main_container {
      display: flex;
    }
    
    .forecast_container {
      display: flex;
      width: 510px;
      height: 130px;
      margin-left: auto;
      margin-right: auto;
      align-items: center;
      text-align: center;
      transition: transform 250ms;
    }
    
    .forecast_slider {
      background-color: black;
      color: white;
      overflow: hidden;
      float: right;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="app"></div>