5
votes

I am trying to migrate my class based react component to react-hooks. The purpose of the component is to fetch stories from HackerNews API and after each 5000 milliseconds to do a polling by hitting the API again for new data.

The problem I am facing is in using the custom hooks below usePrevious() to compare my previous state with current state and only after the comparison to execute some other function inside useEffect() I am most probably missing some basic implementation here of the custom hooks or of useEffect()

And I am following this official guide

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

Here's the code for my class based component and this is perfectly working.

And below is my hooks based component

The problem is with this line

const fromPrevStoriesIds = usePrevious(prevStoriesIds);

The variable fromPrevStoriesIds is giving me good value inside return(), but inside useEffect() its undefined.

import React, { Component, useState, useEffect, useRef } from "react";
import axios from "axios";
import MUIDataTable from "mui-datatables";
import "./Dashboard.css";
import NewItemAddedConfirmSnackbar from "./NewItemAddedConfirmSnackbar";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
const isEqual = require("lodash.isequal");
const differenceWith = require("lodash.differencewith");
const omit = require("lodash.omit");

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

const getEachStoryGivenId = (id, index) => {
  return new Promise((resolve, reject) => {
    axios
      .get(`https://hacker-news.firebaseio.com/v0/item/${id}.json`)
      .then(res => {
        let story = res.data;        
        let result = omit(story, ["descendants", "time", "id", "type"]);        
        if (
          result &&
          Object.entries(result).length !== 0 &&
          result.constructor === Object
        ) {
          resolve(result);
        } else {
          reject(new Error("No data received"));
        }
      });
  });
};

const Dashboard = () => {
  const [prevStoriesIds, setPrevStoriesIds] = useState([]);
  const [fetchedData, setFetchedData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [tableState, setTableState] = useState({});
  const [
    openNewItemAddedConfirmSnackbar,
    setOpenNewItemAddedConfirmSnackbar
  ] = useState(false);
  const [noOfNewStoryAfterPolling, setNoOfNewStoryAfterPolling] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(10);

  const onChangeRowsPerPage = rowsPerPage => {
    setRowsPerPage(rowsPerPage);
  };

  const closeNewItemConfirmSnackbar = () => {
    setOpenNewItemAddedConfirmSnackbar(false);
    axios
      .get("https://hacker-news.firebaseio.com/v0/newstories.json")
      .then(storyIds => {
        setPrevStoriesIds(storyIds.data.slice(0, 2));
        getAllNewStory(storyIds);
      });
  };

  const getAllNewStory = storyIds => {
    setIsLoading(true);
    let topStories = storyIds.data.slice(0, 2).map(getEachStoryGivenId);
    let results = Promise.all(topStories);
    results
      .then(res => {
        setFetchedData(res);
        setIsLoading(false);
      })
      .catch(err => {
        console.log(err);
      });
  };


  const fromPrevStoriesIds = usePrevious(prevStoriesIds);

  useEffect(() => {
    const fetchData = () => {
      axios
        .get("https://hacker-news.firebaseio.com/v0/newstories.json")
        .then(storyIds => {
          //   console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));

          setPrevStoriesIds(storyIds.data.slice(0, 2));
          getAllNewStory(storyIds);
        });
    };
    fetchData();

    const doPolling = () => {
      var timer = setInterval(() => {
        axios
          .get("https://hacker-news.firebaseio.com/v0/newstories.json")
          .then(storyIds => {            
            console.log(
              "fromPrevStoriesIds INSIDE doPolling() ",
              fromPrevStoriesIds
            );

            if (
              fromPrevStoriesIds !== undefined &&
              !isEqual(fromPrevStoriesIds.sort(), storyIds.data.slice(0, 2).sort())
            ) {
              setPrevStoriesIds(storyIds.data.slice(0, 2));
              setNoOfNewStoryAfterPolling(
                differenceWith(
                  prevStoriesIds.sort(),
                  storyIds.data.slice(0, 2).sort(),
                  isEqual
                ).length
              );
              getAllNewStory(storyIds);
              setOpenNewItemAddedConfirmSnackbar(true);              
            }
          });
      }, 5000);
    };

    doPolling();

    // return () => {
    //   console.log("cleaning up");
    //   clearInterval(timer);
    // };
  }, [rowsPerPage, noOfNewStoryAfterPolling]);


  let renderedStoriesOnPage = [];
  const getDataToRender = (() => {
    renderedStoriesOnPage = fetchedData.map(i => {
      return Object.values(i);
    });
    return renderedStoriesOnPage;
  })();


  const columnsOptions = [
    {
      name: "Author",
      sortDirection: tableState
        ? tableState.columns && tableState.columns[0].sortDirection
        : null
    },

    {
      name: "score",
      sortDirection: tableState
        ? tableState.columns && tableState.columns[1].sortDirection
        : null
    },

    {
      name: "title",
      sortDirection: tableState
        ? tableState.columns && tableState.columns[2].sortDirection
        : null
    },

    {
      name: "URL",
      options: {
        filter: false,
        customBodyRender: (value, tableMeta, updateValue) => {
          // console.log("TABLE META IS ", JSON.stringify(tableMeta));
          return (
            <a target="_blank" href={value}>
              {value}
            </a>
          );
        }
      }
    }
  ];

  const options = {
    filter: true,
    selectableRows: false,
    filterType: "dropdown",
    responsive: "stacked",
    selectableRows: "multiple",
    rowsPerPage: tableState ? tableState.rowsPerPage : 10,
    onChangeRowsPerPage: onChangeRowsPerPage,
    activeColumn: tableState ? tableState.activeColumn : 0,
    onTableChange: (action, tableState) => {
      // console.log("taBLE STATE IS ", JSON.stringify(tableState));
      setTableState(tableState);
    }
  };

  return (
    <React.Fragment>
      {console.log("fromPrevStoriesIds INSIDE RETURN --- ", fromPrevStoriesIds)}
      <div
        style={{
          marginLeft: "15px",
          marginTop: "80px",
          display: "flex",
          flexDirection: "row"
        }}
      >
        <h4 style={{ width: "400px", paddingRight: "15px" }}>
          Hacker News top 2
        </h4>
      </div>
      <div>
        {isLoading ? (
          <div className="interactions">
            <div className="lds-ring">
              <div />
              <div />
              <div />
              <div />
            </div>
          </div>
        ) : fetchedData.length !== 0 && renderedStoriesOnPage.length !== 0 ? (
          <MUIDataTable
            title={"Hacker News API top 2 result"}
            data={renderedStoriesOnPage}
            columns={columnsOptions}
            options={options}
          />
        ) : null}
        <NewItemAddedConfirmSnackbar
          openNewItemAddedConfirmSnackbar={openNewItemAddedConfirmSnackbar}
          closeNewItemConfirmSnackbar={closeNewItemConfirmSnackbar}
          noOfNewStoryAfterPolling={noOfNewStoryAfterPolling}
        />
      </div>
    </React.Fragment>
  );
};

export default Dashboard;
1

1 Answers

8
votes

Instead of returning ref.current from usePrevious return, ref since ref.current will be mutated at its reference and you will be able to receive the updated value within useEffect otherwise it will receive the value from its closure

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref;
}

Code:

const fromPrevStoriesIds = usePrevious(prevStoriesIds);

useEffect(() => {
    const fetchData = () => {
      axios
        .get("https://hacker-news.firebaseio.com/v0/newstories.json")
        .then(storyIds => {
          //   console.log("STORY IDs FETCHED ", storyIds.data.slice(0, 2));

          setPrevStoriesIds(storyIds.data.slice(0, 2));
          getAllNewStory(storyIds);
        });
    };
    fetchData();

    const doPolling = () => {
      var timer = setInterval(() => {
        axios
          .get("https://hacker-news.firebaseio.com/v0/newstories.json")
          .then(storyIds => {            
            console.log(
              "fromPrevStoriesIds INSIDE doPolling() ",
              fromPrevStoriesIds.current
            );

            if (
              fromPrevStoriesIds.current !== undefined &&
              !isEqual(fromPrevStoriesIds.current.sort(), storyIds.data.slice(0, 2).sort())
            ) {
              setPrevStoriesIds(storyIds.data.slice(0, 2));
              setNoOfNewStoryAfterPolling(
                differenceWith(
                  prevStoriesIds.sort(),
                  storyIds.data.slice(0, 2).sort(),
                  isEqual
                ).length
              );
              getAllNewStory(storyIds);
              setOpenNewItemAddedConfirmSnackbar(true);              
            }
          });
      }, 5000);
    };

    doPolling();

    // return () => {
    //   console.log("cleaning up");
    //   clearInterval(timer);
    // };
  }, [rowsPerPage, noOfNewStoryAfterPolling]);