React hooks : how to watch changes in a JS class object?

Viewed 2819

I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.

What I understand is that you can get/set a state by using

const [myState, setMyState] = React.useState(myStateValue);

So. My component runs some functions based on the url prop :

const playlist = new PlaylistObj();

React.useEffect(() => {
  playlist.loadUrl(props.url).then(function(){
    console.log("LOADED!");
  })
}, [props.url]);

Inside my PlaylistObj class, I have an async function loadUrl(url) that

  • sets the apiLoading property of the playlist to true
  • gets content
  • sets the apiLoading property of the playlist to false

Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :

  <div
  className={classNames({
    'api-loading': playlist.apiLoading
  })}
  >

But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console. It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?

I tested

const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
  //refresh playlist if its URL is updated
  playlist.loadUrl(props.playlistUrl).then(function(){
    console.log("LOADED!");
  })
}, [props.playlistUrl]);

And this, but it seems more and more unlogical to me, and, well, does not work.

const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
  playlist.loadUrl(props.playlistUrl).then(function(){
    console.log("LOADED!");
    setPlaylist(playlist); //added this
  })
}, [props.playlistUrl]);

I just want my component be up-to-date with the playlist object. How should I handle this ?

I feel like I'm missing something.

Thanks a lot!

2 Answers

I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.

const [playlist, setPlaylist] = React.useState(new PlaylistObj());

React.useEffect(() => {
  playlist.loadUrl(props.playlistUrl).then(function(){
    setPlaylist(playlist); // <-- this playlist reference doesn't change
  })
}, [props.playlistUrl]);

I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.

const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);

React.useEffect(() => {
  setIsLoading(true);
  playlist.loadUrl(props.playlistUrl).then(function(){
    console.log("LOADED!");
    setIsLoading(false);
  });
}, [props.playlistUrl]);

Use the isLoading state in the render

<div
  className={classNames({
    'api-loading': isLoading,
  })}
>

I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".

React.useEffect(() => {
  setIsLoading(true);
  playlist.loadUrl(props.playlistUrl)
    .then(function() {
      console.log("LOADED!");
    })
    .finally(() => setIsLoading(false));
}, [props.playlistUrl]);

Here you go:

import React from "react";

class PlaylistAPI {
  constructor(data = []) {
    this.data = data;
    this.listeners = [];
  }
  addListener(fn) {
    this.listeners.push(fn);
  }

  removeEventListener(fn) {
    this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
  }

  setPlayList(data) {
    this.data = data;

    this.notif();
  }

  loadUrl(url) {
    console.log("called loadUrl", url, this.data)
  }

  notif() {
    this.listeners.forEach(fn => fn());
  }
  
}

export default function App() {
  const API = React.useMemo(() => new PlaylistAPI(), []);

  React.useEffect(() => {
    API.addListener(loadPlaylist);

    /**
     * Update your playlist and when user job has done, listerners will be called 
     */
    setTimeout(() => {
      API.setPlayList([1,2,3])
    }, 3000)

    return () => {
      API.removeEventListener(loadPlaylist);
    }
  }, [API])

  function loadPlaylist() {
    API.loadUrl("my url");
  }

  return (
    <div className="App">
      <h1>Watching an object by React Hooks</h1>
    </div>
  );
  }

Demo in Codesandbox

Related