progpro
progpro

Reputation: 195

React: How to hit API only once using useEffect

I have a array of objects in API call which I'm calling from useEffect. Basically what I want is that, I want to hit the API only once and set the array in localStorage.

Now, if localStorage has some data related to it the set array using useState will be set in it through localStorage otherwise if it doesn't contain any item related to it, then it would hit API again to fetch the array and set to localStorage.

The code what I have done so far is:

const [loadedg, setloadedg] = useState(false);
const [domainListGroupItems, setdomainListGroupItems] = useState([]);

    useEffect(() => {
        //---------->Groups Lists<----------------
        if(!loadedg) {
            setdomainListGroupItems(JSON.parse(localStorage.getItem('leftgroup')))
        }else{
            ApiService(leftgroups).then((response) => {
                if (response.statusCode === 1) {
                    const item = response.domainGroupList
                    localStorage.setItem('leftgroup', JSON.stringify(item));
                    setloadedg(true);
                    console.log(item);
                } else {
                    setloadedg(false);
                }
            })
            .catch((error) => {
                console.log(error)
            })
        }
    }, [loadedg]);

The concept which I was using here is basically using boolean condition which I think I should be using. But if it can be done without using the boolean value, that would be great for me.

How will I resolve this?. Thank you.

Upvotes: 1

Views: 1358

Answers (3)

webketje
webketje

Reputation: 10976

So you want to use localStorage as client cache, it's more interesting to set a date for expiry than to check for existence of the key. In the code snippet below I shimmed localStorage as ls and the API call as a promise with setTimeout for testing purposes.

I extracted the apiCall effect as standalone, so you can use it anywhere, and gave it a skip parameter. In the snippet, the call will be skipped if the localstorage cache dates from less than 7 days ago. You can edit the snippet and uncomment the 2 lines to test what happens when the cache is recent. Be sure to expand the snippet to "Full page" else the console logs hide the localStorage value :)

const useState = React.useState,
  useEffect = React.useEffect;

// network call shim
function ApiService() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        resolve({
          statusCode: 1,
          domainGroupList: ['Hello','World']
        });
      } catch (err) {
        reject(new Error('500 Server Error'));
      }
    }, 1000);
  });
}

// localstorage shim
const ls = Object.create({
  getItem(key) {
    return this[key];
  },
  setItem(key, value) {
    this[key] = value;
  }
});

// comment out to see what happens when ls has been populated less than 7d ago
// ls.setItem('lastfetch', new Date().toISOString());
// ls.setItem('groupleft', []);

function isLessThan7DaysAgo(date) {
    const today = new Date();
    return date > new Date(today.setDate(today.getDate()-7));
}

const apiCallEffect = (onLoad, skip) => () => {
  if (skip) return;

  ApiService().then((response) => {
      if (response.statusCode === 1) {
        const items = response.domainGroupList;
        ls.setItem('leftgroup', JSON.stringify(items));
        ls.setItem('lastfetch', new Date().toISOString());
        if (onLoad)
          onLoad(items);
      }
    })
    .catch((error) => {
      console.log(error.message)
    });
}

const App = ({
    initialItems,
    onLoad
  }) => {
    const skip = ls.getItem('lastfetch') && isLessThan7DaysAgo(new Date(ls.getItem('lastfetch')));
    const [loaded, setloaded] = useState(skip);
    const [groupItems, setGroupItems] = useState(initialItems);
 
    console.log(skip ? 'skipping fetch, getting from ls' : 'doing fetch, storing in ls');

    useEffect(apiCallEffect((items) => {
      setloaded(true);
      setGroupItems(items);
    }, skip), [groupItems]);

    return <div>
     { loaded ? <ul>{groupItems.map(x => (<li>{x}</li>)) }</ul> : <div>Loading...</div> }
     <strong>In localstorage</strong><br/>
     <pre>{JSON.stringify(ls, null, 2)}</pre>
    </div>;
  };

const storedDomainListGroupItems = ls.getItem('leftgroup') || [];
ReactDOM.render(<App initialItems={storedDomainListGroupItems}/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<strong>In app:</strong>
<div id="app"></div>

Upvotes: 1

pshrimal000
pshrimal000

Reputation: 97

To limit useEffect to fire only once, you can remove the loadedg from depedency array, now it will simply check if data is already there in local storage or not, if not, it will fetch the data and it will set the data in local storage.

Link for article regarding useEffect and dependency array

Hope it solves your problem.

Upvotes: -1

dbuchet
dbuchet

Reputation: 1651

You can initialize your state with a value

const [domainListGroupItems, setdomainListGroupItems] = useState(JSON.parse(localStorage.getItem('leftgroup')));

and then, in your useEffect, just checking if domainListGroupItems is null or not

useEffect(() => {
  if (domainListGroupItems) return; // already set from localStorage

  ApiService(leftgroups).then((response) => { .... })

}, [domainListGroupItems]);

Upvotes: 0

Related Questions