Nick09
Nick09

Reputation: 228

Delayed state change in hook

So I've recently started to change some functionalities into hook's, one of which is supposed to load some components from a slides array, get a position number from currentSlide and then create a tag name combining these two.

import React, { useReducer, useRef } from "react";

const initialState = {
    slides: ['Slide1','Slide2','Slide3'],
    currentSlide: 0,
    initialX: 0,
    endX: 0
}

const reducer = (state, action) => {
    switch (action.type) {
        case 'pull-slides':
            return { ...state, slides: action.value };
        case 'swipe-right':
            return { ...state, currentSlide: state.currentSlide + 1 }
        case 'swipe-left':
            return { ...state, currentSlide: state.currentSlide - 1 }
        case 'set-initialX':
            return {
                ...state, initialX: action.position
            }
        case 'set-endX':
            return {
                ...state, endX: action.position
            }
        default:
            throw new Error('Unexpected action');
    }
}

const swipeDetection = () => {
    const [state, dispatch] = useReducer(reducer, initialState)
    const swipeArea = useRef(null)

    function _onMouseDown(e) {
        console.log(e.screenX)
        dispatch({ type: 'set-initialX', position: e.screenX })
        console.log(state)
    }

    function _onMouseUp(e) {
        console.log(e.screenX)
        dispatch({ type: 'set-endX', position: e.screenX })
        console.log(state)

        if ((state.initialX < state.endX) && ((state.endX - state.initialX) > 350)) {
            console.log('right')
            if (state.currentSlide < (state.slides.length - 1)) {
                console.log('right')
                dispatch({ type: 'swipe-right' })
            }
        } else if ((state.initialX > state.endX) && ((state.initialX - state.endX) > 350)) {
            console.log('left')
            if (state.currentSlide > 0) {
                dispatch({ type: 'swipe-left' })
               console.log('left')
            }
        }
    }

    const TagName = state.slides[state.currentSlide]

    return (
        typeof TagName == 'function' ? (
            <div className="h-100" ref={swipeArea} onMouseDown={_onMouseDown} onMouseUp={_onMouseUp}>
                <TagName />
            </div>
        ) : ''
     )
}

export default swipeDetection

In order to switch from one component to another I decided to use onMouseDown/onMouseUp instead of onMouseMove events.

So when you click down it stores the screenX and when you release the mouse is stores the screenX again and compares the two values to see how far you've swiped left or right.

This seems to work pretty well from what I could tell, except for the fact that you have to swipe in one direction twice in order for it to actually change the currentSlide value.

I have no idea why this is happening, granted I am really new to Hooks and I'm just getting my code-legs (like see-legs but for programmers?).

I've tried different approaches and materials but I couldn't really find anything relevant to my case, any ideas for why something like this would happen?

I've added the console.log for e.screenX and state to check and see what is happening on click, so when you click it does immediately return the e.screenX value and the state, but the state is not updated with the value for some reason...

Upvotes: 2

Views: 775

Answers (1)

Jonas Wilms
Jonas Wilms

Reputation: 138235

dispatch (just like setState and any other call to React to trigger an update) gets deferred, so it won't run immeadiately. Additionally dispatching causes a rerender, which will execute the whole component function again, which will create a new state variable inside of it. No matter what you do, state will always point to the old state. Therefore this:

   state.initialX < state.endX

will always operate on the previous state. To solve this do the following:

1) Remove endX and endY from the state. If the finger gets raised you want to check for a swipe immeadiately. There is no need for persisting that position.

2) There are just two events needed for your reducer: touchdown and touchup. Everything else should be done inside the reducer.

    const SlideSwiper = ({ slides }) => {
      const [{ currentSlide }, touch] = useReducer(({ currentSlide, start }, { type, position }) => {
         if(type === "up") {
           if(Math.abs(position.x - start.x) < 350 && Math.abs(position.y - start.y) < 350)
             return { currentSlide };
           }

           // TODO: Additional logic for swiping up & down?

           return { currentSlide: currentSlide + (position.x > start.x ? 1 : -1), start: null };

         } else if(type === "down") {
           return { currentSlide, start: position };
         }
      }, { currentSlide: 0, start: null });

      const touchDown = e => touch({ type: "down", position:  { x: e.clientX, y: e.clientY }});
      const touchUp = e => touch({ type: "up", position:  { x: e.clientX, y: e.clientY }});

      const slide = slides[currentSlide];
      //...
  });

Upvotes: 1

Related Questions