MSOACC
MSOACC

Reputation: 3665

React context state updates are always one step behind

I have read questions with similar titles and they have not solved my problem.

I have an API call whose result needs to be shared amongst several components. The parent component makes the call and React's context is used to share it amongst the child components:

MainPage.js:

import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { VideoPlayer } from "../components/VideoPlayer";
import VideoContext from "../components/VideoContext";


export default function Watch() {
    const [video, setVideo] = useState({});
    const { videoHash } = useParams();
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        setIsLoading(true);
        getVideo();
    }, [videoHash]);

    const getVideo = async () => {
        if(videoHash) {
            const res = await getVideoFromApi();
            setIsLoading(false);
            // The first time this runs nothing happens, the video can't be played
            // The second time it runs (i.e. when the URL/videoHash changes) it updates 
            // but it shows the video from the first run
            setVideo(res.video);
        }
    };

    return (
        <>
            {isLoading ? (
                <div>Loading...</div>
            ) : (
                <VideoContext.Provider value={{ video, setVideo }}>
                    <VideoPlayer videoHash={videoHash} />
                </VideoContext.Provider>
            )}
        </>
    );
}

VideoPlayer.js:

import React, { useState, useEffect, useContext } from "react";
import VideoContext from "./VideoContext";
import styles from "./VideoPlayer.module.css";

export function VideoPlayer({ videoHash }) {
    const { video, setVideo } = useContext(VideoContext);
    const [forceRender, setforceRender] = useState(true);

    useEffect(() => {
        // I tried adding this to no effect
        setforceRender(!forceRender);
    }, [videoHash]);

    return (
        <video controls className={styles["video-player"]}>
            <source src={video.VideoUrl} type="video/mp4" />
                Sorry, your browser does not support embedded videos.
        </video>
    );
}

VideoContext.js:

import { createContext } from "react";

export default createContext({
    video: {}, 
    setVideo: () => {}
});

It works when the page loads, but when my Link components change the videoHash property the new video loads (I can see when I console.log() the API call) but it does not update in the video player.

The second time a link is clicked and the videoHash param is changed, the video displays but it's for the previous video.

Upvotes: 0

Views: 588

Answers (1)

Dakota Lee Martinez
Dakota Lee Martinez

Reputation: 621

https://codesandbox.io/s/blazing-lake-k4i8n?file=/src/VideoPlayer.js

Unless I'm missing something, I think the VideoPlayer would be all right just behaving as a functional component without any hooks for state, that could be handled by Watch. When you click on a link to another route that will point to watch, the videoHash will change

VideoPlayer.js

import React from "react";
import { Link } from "react-router-dom";

export function VideoPlayer({ videoHash }) {
  // const { video, setVideo } = useContext(VideoContext);
  // const [forceRender, setforceRender] = useState(true);

  // useEffect(() => {
  //   // I tried adding this to no effect
  //   setforceRender(!forceRender);
  // }, [videoHash]);
  // console.log(video);
  // Am I missi
  return (
    <div>
      Am I missing something or could you just use your videoHash: {videoHash},
      here?
      <Link to="/watch/a">Previous</Link>
      <Link to="/watch/c">Next</Link>
    </div>
  );
}

Watch.js

import React, { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import { VideoPlayer } from "./VideoPlayer";
import VideoContext from "./VideoContext";

export default function Watch() {
  const [video, setVideo] = useState({});
  const { videoHash } = useParams();
  const [isLoading, setIsLoading] = useState(true);

  const getVideo = useCallback(async () => {
    if (videoHash) {
      const res = await getVideoFromApi();
      setTimeout(() => {
        setIsLoading(false);
        setVideo(res);
      }, 1000);
    }
  }, [videoHash]);

  useEffect(() => {
    setIsLoading(true);
    getVideo();
  }, [getVideo]);

  const getVideoFromApi = async () => {
    const videoArray = ["A", "B", "C"];
    const randomItem =
      videoArray[Math.floor(Math.random() * videoArray.length)];
    return Promise.resolve(randomItem);
  };

  return (
    <>
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <VideoContext.Provider value={{ video, setVideo }}>
          <VideoPlayer videoHash={videoHash} />
        </VideoContext.Provider>
      )}
    </>
  );
}

VideoContext.js

import { createContext } from "react";

export default createContext({
  video: "",
  setVideo: () => {}
});

I added a timeout so you can see the loading part work as well. Let me know if there's something I'm missing about what you need to do.

Upvotes: 1

Related Questions