Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access old state to compare with new state inside useEffect react hook with custom hooks usePrevious

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;
like image 555
Rohan_Paul Avatar asked Jun 06 '19 16:06

Rohan_Paul


People also ask

Can I get previous state in useEffect?

While there's currently no React Hook that does this out of the box, you can manually retrieve either the previous state or props from within a functional component by leveraging the useRef , useState , usePrevious , and useEffect Hooks in React.

How do you get the previous state value in React hooks?

With React class components you have the componentDidUpdate method which receives previous props and state as arguments or you can update an instance variable (this. previous = value) and reference it later to get the previous value.

How do you update state with previous state using a new state hook?

To update the state, call the state updater function with the new state setState(newState) . Alternatively, if you need to update the state based on the previous state, supply a callback function setState(prevState => newState) .

Is it possible to use a custom hook inside useEffect in React?

Lastly, we have been able to create a custom reusable hook with simple logic. With the React useEffect hook, you can manage component lifecycle seamlessly without necessarily having to convert your functional based components into class based components.


1 Answers

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]);
like image 169
Shubham Khatri Avatar answered Sep 20 '22 00:09

Shubham Khatri