1

So i'm working on this feedback board website. And i have this roadmap route where users get to see what is the website improvement roadmap. And on Mobile i would like to have a three section slide : planned , in-progress and live So i got this code from stackOverflow that enables me to detect touch sliding events. So my goal was to navigate from a roadmap stage to another whenever the user swipes left or right.

BUT whenever i swip to "Live" or "Planned" section ,and then try go go back to the "In-Progress" section , it jumps it and go directly to the section after it

To repreduce this : here is the live link

  1. set Mobile mode using the dev tools
  2. click on the menu icon
  3. click on the view link of the roadmap div to navigate to the roadmap route
  4. navigate to "Live" using the touch swiping "in this case the mouse"
  5. try to go back to the "In-Progress" section

here is the event listiners that i used:

    document.querySelector('#roadmap_stages').addEventListener('gesture-right',()=>{
      onScroll("roadmap_stages","r")
      switch(currentStage){
        case "In-Progress":
          dispatch(setcurrentStage("Planned"))
        break;
        case "Live":
          dispatch(setcurrentStage("In-Progress"))
        break;
      }
    })
    document.querySelector('#roadmap_stages').addEventListener('gesture-left',()=>{
      onScroll("roadmap_stages","l")
      switch(currentStage){
        case "Planned":
          dispatch(setcurrentStage("In-Progress"))
        break;
        case "In-Progress":
          dispatch(setcurrentStage("Live"))
        break;
      }
    })

here is the onScroll function : the goal of this function is to take care of the animations

  function onScroll (id,direction){
    const navigater = document.querySelector(".roadmap_roadmap_stages__FAUDD")
    const currentPosition = window.getComputedStyle(navigater).left
    let to;

    if(direction === "r"){
      switch (currentStage){
        case "Planned": 
          to = ""
        break;
        case "In-Progress":
          to = "one"
        break;
        case "Live":
          to = "two"
        break;
      }
    }
    else{
      switch (currentStage){
        case "Planned": 
          to = "two"
        break;
        case "In-Progress":
          to = "three"
        break;
        case "Live":
          to = ""
        break;
      }
    }

    navigater.style.left = `${currentPosition}`
    navigater.style.animationName = `${to}`
  }

here is my redux slice:

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    screenWidth:null,
    isMenuOpen:false,
    isSortOpen:false,
    sortMethode:"Most Upvotes",
    filter:"all",
    currentStage:"In-Progress",
}

const uiSlice = createSlice({
    name:"ui",
    initialState,
    reducers:{
        setScreenWidth:(state,{payload})=>{
            state.screenWidth = payload
        },
        toggleMenu:(state,{payload})=>{
            state.isMenuOpen = payload
        },
        toggleSort:(state,{payload})=>{
            state.isSortOpen = payload
        },
        setSortMethode:(state,{payload})=>{
            state.sortMethode = payload
        },
        setFilter:(state,{payload})=>{
            state.filter = payload
        },
        setcurrentStage:(state,{payload})=>{
            console.log(state.currentStage)
            state.currentStage = payload
            console.log(state.currentStage)
        },
    }
})

export default uiSlice.reducer
export const {setScreenWidth,toggleMenu,toggleSort,setSortMethode,setFilter,setcurrentStage} = uiSlice.actions

and here are the animations

@keyframes one {
    100%{left: 0;}
}
@keyframes two {
    100%{left: -100%;}
}
@keyframes three {
    100%{left: -200%;}
}

and here is the whole function just for reference :

import React, { useEffect, useRef } from 'react'

//components
import Stage from './Stage'

//styles
import styles from "@/styles/css/roadmap.module.css"

//state
import { useDispatch, useSelector } from 'react-redux'
import { store } from '@/state/store'
import { setcurrentStage } from '@/state/slices/uiSlice' 

export default function RoadmapStages(props) {
  const {planned,inProgress,live} = props.roadmapData
  const stages = useRef(null)
  const dispatch = useDispatch()
  const currentStage = store.getState().ui.currentStage

  // dispatch(setcurrentStage("tagopi"))
  function onScroll (id,direction){
    const navigater = document.querySelector(".roadmap_roadmap_stages__FAUDD")
    const currentPosition = window.getComputedStyle(navigater).left
    let to;

    if(direction === "r"){
      switch (currentStage){
        case "Planned": 
          to = ""
        break;
        case "In-Progress":
          to = "one"
        break;
        case "Live":
          to = "two"
        break;
      }
    }
    else{
      switch (currentStage){
        case "Planned": 
          to = "two"
        break;
        case "In-Progress":
          to = "three"
        break;
        case "Live":
          to = ""
        break;
      }
    }

    navigater.style.left = `${currentPosition}`
    navigater.style.animationName = `${to}`
  }

  useEffect(()=>{

    //mobile-scrolling-event-listener
    (function(d) {
      // based on original source: https://stackoverflow.com/a/17567696/334451
      var newEvent = function(e, name) {
          // This style is already deprecated but very well supported in real world: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
          // in future we want to use CustomEvent function: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
          var a = document.createEvent("CustomEvent");
          a.initCustomEvent(name, true, true, e.target);
          e.target.dispatchEvent(a);
          a = null;
          return false
      };
      var debug = false; // emit info to JS console for all touch events?
      var active = false; // flag to tell if touchend should complete the gesture
      var min_gesture_length = 20; // minimum gesture length in pixels
      var tolerance = 0.3; // value 0 means pixel perfect movement up or down/left or right is required, 0.5 or more means any diagonal will do, values between can be tweaked
  
      var sp = { x: 0, y: 0, px: 0, py: 0 }; // start point
      var ep = { x: 0, y: 0, px: 0, py: 0 }; // end point
      var touch = {
          touchstart: function(e) {
              active = true;
              var t = e.touches[0];
              sp = { x: t.screenX, y: t.screenY, px: t.pageX, py: t.pageY };
              ep = sp; // make sure we have a sensible end poin in case next event is touchend
              debug && console.log("start", sp);
          },
          touchmove: function(e) {
              if (e.touches.length > 1) {
                  active = false;
                  debug && console.log("aborting gesture because multiple touches detected");
                  return;
              }
              var t = e.touches[0];
              ep = { x: t.screenX, y: t.screenY, px: t.pageX, py: t.pageY };
              debug && console.log("move", ep, sp);
          },
          touchend: function(e) {
              if (!active)
                  return;
              debug && console.log("end", ep, sp);
              var dx = Math.abs(ep.x - sp.x);
              var dy = Math.abs(ep.y - sp.y);
  
              if (Math.max(dx, dy) < min_gesture_length) {
                  debug && console.log("ignoring short gesture");
                  return; // too short gesture, ignore
              }
  
              if (dy > dx && dx/dy < tolerance && Math.abs(sp.py - ep.py) > min_gesture_length) { // up or down, ignore if page scrolled with touch
                  newEvent(e, (ep.y - sp.y < 0 ? 'gesture-up' : 'gesture-down'));
                  //e.cancelable && e.preventDefault();
              }
              else if (dx > dy && dy/dx < tolerance && Math.abs(sp.px - ep.px) > min_gesture_length) { // left or right, ignore if page scrolled with touch
                  newEvent(e, (ep.x - sp.x < 0 ? 'gesture-left' : 'gesture-right'));
                  //e.cancelable && e.preventDefault();
              }
              else {
                  debug && console.log("ignoring diagonal gesture or scrolled content");
              }
              active = false;
          },
          touchcancel: function(e) {
              debug && console.log("cancelling gesture");
              active = false;
          }
      };
      for (var a in touch) {
          d.addEventListener(a, touch[a], false);
          // TODO: MSIE touch support: https://github.com/CamHenlin/TouchPolyfill
      }
    })(window.document);

    document.querySelector('#roadmap_stages').addEventListener('gesture-right',()=>{
      onScroll("roadmap_stages","r")
      switch(currentStage){
        case "In-Progress":
          dispatch(setcurrentStage("Planned"))
        break;
        case "Live":
          dispatch(setcurrentStage("In-Progress"))
        break;
      }
    })
    document.querySelector('#roadmap_stages').addEventListener('gesture-left',()=>{
      onScroll("roadmap_stages","l")
      switch(currentStage){
        case "Planned":
          dispatch(setcurrentStage("In-Progress"))
        break;
        case "In-Progress":
          dispatch(setcurrentStage("Live"))
        break;
      }
    })
  },[])

  return (
    <div ref={stages} className={styles.roadmap_stages} id="roadmap_stages" >
        <Stage stageData={planned} />
        <Stage stageData={inProgress} />
        <Stage stageData={live} />
    </div>
  )
}

here is the github link

i was and im still stuck on this bug for two days and would highly any help from the stackOverflow community

thanks a lot :)

TAGOPI
  • 13
  • 5
  • Have you tried `const currentStage = useSelector(state => state.ui.currentStage)`? – Unmitigated Mar 04 '23 at 20:08
  • thanks for considering responding to me . the useSelectore() methode was the first aproche i toke ,but i have read somwhere that using store.getState() might be better , but in both cases it didn't work :( – TAGOPI Mar 04 '23 at 20:34
  • What happens? Is there an error in the console? – Unmitigated Mar 04 '23 at 20:39
  • i have jusssst added a live link to the website ,as you will see the roadmap route in mobile mode doesn't work as expected – TAGOPI Mar 04 '23 at 22:33
  • What state isn't updating and how exactly are you validating/verifying this? It's not clear from your description what any issue is. Can you [edit] to clarify exactly what the issue is and what you have tried already to address it? Is it the `currentStage` state? I'm just guessing since it's referenced in some callbacks you included and you have logging around this state in the reducer case. – Drew Reese Mar 04 '23 at 22:43

1 Answers1

0

As far as I can tell this is an issue of stale closure over the currentStage state. Firstly, the RoadmapStages component isn't subscribed to app's redux store, so it is not going to be notified of any changes. Secondly, the onScroll callbacks are never re-instantiated to close over any updated currentStage values.

I suggest the following refactor to (A) subscribe the component to redux state changes and (B) correctly handle instantiating the callbacks and cleaning up effects.

export default function RoadmapStages(props) {
  const { planned, inProgress, live } = props.roadmapData;

  const stages = useRef(null);

  const dispatch = useDispatch();
  const { currentStage } = useSelector(state => state.ui);

  useEffect(() => {
    // mobile-scrolling-event-listener
    
    /* Return any necessary mobile touch/scroll handlers if necessary */
  }, []);

  useEffect(() => {
    function onScroll (id, direction) {
      const navigater = document.querySelector(".roadmap_roadmap_stages__FAUDD");
      const currentPosition = window.getComputedStyle(navigater).left
      let to;

      if (direction === "r") {
        switch (currentStage){
          case "Planned": 
            to = ""
            break;
          case "In-Progress":
            to = "one"
            break;
          case "Live":
            to = "two"
            break;
        }
      } else {
        switch (currentStage){
          case "Planned": 
            to = "two"
            break;
          case "In-Progress":
            to = "three"
            break;
          case "Live":
            to = ""
            break;
        }
      }

      navigater.style.left = `${currentPosition}`
      navigater.style.animationName = `${to}`
    }

    const handleRightGesture = () => {
      onScroll("roadmap_stages", "r");
      switch(currentStage) {
        case "In-Progress":
          dispatch(setcurrentStage("Planned"));
          break;
        case "Live":
          dispatch(setcurrentStage("In-Progress"));
          break;
      }
    };

    const handleLeftGesture = () => {
      onScroll("roadmap_stages", "l");
      switch(currentStage) {
        case "Planned":
          dispatch(setcurrentStage("In-Progress"));
          break;
        case "In-Progress":
          dispatch(setcurrentStage("Live"));
          break;
      }
    }

    stages.current.addEventListener('gesture-right', handleRightGesture);
    stages.current.addEventListener('gesture-left', handleLeftGesture);

    return () => {
      stages.current.removeEventListener('gesture-right', handleRightGesture);
      stages.current.removeEventListener('gesture-left', handleLeftGesture);
    };
  }, [currentStage]);

  return (
    <div ref={stages} className={styles.roadmap_stages} id="roadmap_stages" >
      <Stage stageData={planned} />
      <Stage stageData={inProgress} />
      <Stage stageData={live} />
    </div>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • thank you so so so much for your answer Drew. I have added the changes to the code as you said ( adding a clean up funcion , adding "currentStage as an a dependencie in the useEffect ) wich for sure optimize the code . But the porbleme unfortunately hasen't been solved. As it still skips the "In-progress" section and goes directly to the "live" section if i swip right or the "planned" section if i swip left and the only way to acces the "In-Progress" section is to click on the "navigation bar" on the top . You could see that if you took the (bug reproducing steps just added ) . Thanks – TAGOPI Mar 04 '23 at 23:33
  • @TAGOPI Think you could create a ***running*** [codesandbox](https://codesandbox.io/) demo that reproduces the issue that we could inspect live? – Drew Reese Mar 04 '23 at 23:47
  • @TAGOPI I've forked your repo into a sandbox and applied my suggested solution above. What I can see is that for each "swipe" gesture two right or tow left gesture actions are dispatched. What I mean by this is that I correctly see the `currentState` state update to the middle stage but then an additional gesture action is dispatched in the direction of the swipe. I am taking a deeper look. – Drew Reese Mar 04 '23 at 23:59
  • ooooook , so that's why it skipped the "in-progress" section .I also saw that when i tried to solve the probleme by my self , i console.log the currentState , and i saw multiple values per on swip.But i thought that it was cause by rect strict mode double rendering or something lik that. i will make you know if i come to a solution . and im looking forward to your observations. thanks a loooot Drew by the way . – TAGOPI Mar 05 '23 at 00:12
  • 1
    So i have found a work aroud this bug where instead of getting the currentStage from redux , i concluded it from the currentPosition of the div using the value of css left offside. Thanks anyway for your collaboration .and i will aprove your answer since you helped me optimize the code and that i already found a solution. Keep on Coding ! – TAGOPI Mar 05 '23 at 03:40