Search code examples
javascriptreactjswebreduxmobile

Redux State won't update via dispatch


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 :)


Solution

  • 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>
      );
    }