Really weird behavior of the react hook useState()

I am currently writing a react app using Swiper for a simple slideshow and everything works fine. I am now trying to improve the navigation by displaying the current slide with the setActiveSlide function. After the first loading, it works fine. After clicking the buttons that call setActiveSlide, the story object is set to null, which results in an error (Uncaught TypeError: Cannot read property 'slideNext' of null) when you click the Previous or Next buttons a second time. Why is the story object set to null after calling setActiveSlide? Please help

Here is a simplified version of the app

import React, { useState, useEffect, useRef } from "react";
import Swiper from "swiper";

const Slideshow = props => {
    var swiper = useRef();
    var story = null;
    const [activeSlide, setActiveSlide] = useState("");

    useEffect(() => {
        story = new Swiper(swiper.current, {
            loop: false,
            speed: 1100
        });
    }, []);

    const prevSlide = () => {
        setActiveSlide("PREV");
        story.slidePrev();
    };

    const nextSlide = () => {
        setActiveSlide("NEXT");
        story.slideNext();
    };

    return (
        <div className="container-story" ref={swiper}>
            <div className="container-story-wrapper swiper-wrapper">
                {props.children}
            </div>

            <div className="navigation">
                <button onClick={prevSlide}> Previous </button>
                <button onClick={nextSlide}> Next </button>

                {activeSlide}
            </div>
            <div className="lightbox-container"></div>
        </div>
    );
};
export default Slideshow; 

Upvotes: 2

Views: 962

Answers (2)

Beginner
Beginner

Reputation: 9125

So the problem is

Assignments to the 'story' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.

So you need to update as follow

import React, { useState, useEffect, useRef } from "react";
import "./styles.css";

import Swiper from "swiper";

const Slideshow = props => {
  var swiper = useRef();
  var story = useRef(null);
  const [activeSlide, setActiveSlide] = useState("");

  useEffect(() => {
    story.current = new Swiper(swiper.current, {
      loop: false,
      speed: 1100
    });
  }, []);

  const prevSlide = () => {
    setActiveSlide("PREV");
    story.current.slidePrev();
  };

  const nextSlide = () => {
    setActiveSlide("NEXT");
    story.current.slideNext();
  };

  return (
    <div className="container-story" ref={swiper}>
      <div className="container-story-wrapper swiper-wrapper">
        {props.children}
      </div>

      <div className="navigation">
        <button onClick={prevSlide}> Previous </button>
        <button onClick={nextSlide}> Next </button>

        {activeSlide}
      </div>
      <div className="lightbox-container" />
    </div>
  );
};
export default Slideshow;

Working codesandbox

Upvotes: 0

ApplePearPerson
ApplePearPerson

Reputation: 4439

This is a function component, so if you want a variable to persist through multiple renders then you cannot declare it locally like you do with var story = null. Your first render will have it set but when the component re-renders you set it back to null.

The proper way to deal with this would be to use Reacts useRef hook. This is a way to have a variable persist through multiple renders.

To declare it: const _story = useRef(null);

To read/write to the _story variable you will need to access _story.current. So in your useEffect mount you should use this instead:

_story.current = new Swiper(swiper.current, {
    loop: false,
    speed: 1100
});

And in your handlers: _story.current.slideNext();

You can read more about useRef here.

Upvotes: 5

Related Questions