REST API Response Flickering on Page Load

Viewed 408

I've got component that displays contact information from a dealer as chosen by the user. To be more specific, a user selects their location, setting a cookie which then is used to define the API call. I pull in the contact information of the dealer in that location using Axios, store it in a context, and then display the information as necessary through several components: the header, a "current location" component etc. However, I'm having an issue with the content flickering each time the page is refreshed.

I've tried storing the JSON response in local storage, but, for a brief moment on page load, it shows as undefined, making the flicker continue. Obviously, I'm needing to eliminate that so that the data persists.

I've got it working via ApiContext, and I'm displaying the data in my Header component. Below is the code for both:

ApiContext.tsx

import React, { createContext, useEffect, useState } from 'react';
import axios from 'axios';

const contextObject = {} as any;

export const context = createContext(contextObject);

export const ApiContext = ({ children }: any) => {
  const [selectedDealer, setselectedDealer] = useState(`1`);

  useEffect(() => {
    axios
      .get(`${process.env.GATSBY_API_ENDPOINT}/${selectedDealer}`)
      .then((response) => setselectedDealer(response.data));
  }, [selectedDealer]);

  const changeDealer = (id: any) => {
    setselectedDealer(id);
  };

  const { Provider } = context;
  return (
    <Provider value={{ data: selectedDealer, changeDealer: changeDealer }}>
      {children}
    </Provider>
  );
};

Header.tsx

import React, { ReactNode, useContext, useEffect, useState } from 'react';

import Logo from 'assets/svg/logo.svg';
import css from 'classnames';
import { Button } from 'components/button/Button';
import { Link } from 'components/link/Link';
import { MenuIcon } from 'components/menu-icon/MenuIcon';
import { context } from 'contexts/ApiContext';

import { NotificationBar } from '../notification-bar/NotificationBar';
import s from './Header.scss';
import { MainNav } from './navigation/MainNav';

interface HeaderProps {
  navigationContent: ReactNode;
}

export const Header = ({ navigationContent }: HeaderProps) => {
  const [scrolled, setScrolled] = useState(false);
  const [open, setOpen] = useState(false);

  const data = useContext(context);
  
  const buttonLabel = data ? data.name : 'Find a Dealer';
  const buttonLink = data ? `tel:${data.phone}` : '/find-a-dealer';

  useEffect(() => {
    const handleScroll = () => {
      const isScrolled = window.scrollY > 10;
      if (isScrolled !== scrolled) {
        setScrolled(!scrolled);
      }
    };

    document.addEventListener('scroll', handleScroll, { passive: true });

    return () => {
      document.removeEventListener('scroll', handleScroll);
    };
  }, [scrolled]);

  return (
    <>
      <NotificationBar notificationContent={navigationContent} />
      <header className={scrolled ? css(s.header, s.header__scrolled) : s.header}>
        <nav className={s.header__navigation}>
          <ul className={s.header__container}>
            <li className={s.header__logo}>
              <Link to="/" className={s.header__link}>
                <Logo />
              </Link>
            </li>

            <li className={s.header__primary}>
              <MainNav navigationItems={navigationContent} />
            </li>

            <li className={s.header__utility}>
              <Button href={buttonLink}>{buttonLabel}</Button>
            </li>

            <li className={s.header__icon}>
              <MenuIcon onClick={() => setOpen(!open)} />
            </li>
          </ul>
        </nav>
      </header>
    </>
  );
};

I would assume that this is because the API call is being triggered each time the page is refreshed, so I wonder if there's any way to persist the data in a more efficient way?

Thanks in advance!

2 Answers

Your ApiContext.tsxcould persist the data in localStorage is such a way:

import React, { createContext } from 'react';

import axios from 'axios';
import { makeUseAxios } from 'axios-hooks';
import { useCookie } from 'hooks/use-cookie';

const contextObject = {} as any;

export const context = createContext(contextObject);

const useAxios = makeUseAxios({
  axios: axios.create({ baseURL: process.env.GATSBY_API_ENDPOINT }),
});

const loadData = (cookie) => {
  const stored = localStorage.getItem("data");
  const parsed = JSON.parse(stored);
  // You can also store a lastSync timestamp along with the data, so that you can refresh them if necessary
  if (parsed) return parsed;
  const [{data}] = useAxios(`${cookie}`);
  if (!isEqual(parsed, data)) {
    localStorage.setItem('data', JSON.stringify(data));
  }
  return data
}

export const ApiContext = ({ children }: any) => {
  const [cookie] = useCookie('one-day-location', '1');
  const [{ data }] = loadData(cookie);

  const { Provider } = context;
  return <Provider value={data}>{children}</Provider>;
};

The above implementation will only fetch the data once, so remember to refresh them at some point inside your code and update the localStorage item, or use a timestamp to compare and force the api call as commented in my code.

Keep in mind that even this implementation may take a fraction of a second to be completed, so I would suggest to always use loaders/spinners/skeletons while your application is fetching the required data.

I got this worked out, using a hook that persists my state, storing it in a localStorage item.

usePersistState.ts

import { useEffect, useState } from 'react';

export const usePersistState = (key: string, defaultValue: string) => {
  const [value, setValue] = useState(() => {
    if (typeof window !== 'undefined') {
      const stickyValue = window.localStorage.getItem(key);
      return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
    }
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
};

Then, in ApiContext, I set my default state, but when that state changes, it updates and persists the state. Here's my context component now:

ApiContext.tsx

import React, { createContext, useEffect } from 'react';

import { usePersistState } from 'hooks/use-persist-state';

import axios from 'axios';

const contextObject = {} as any;

export const context = createContext(contextObject);

const LOCAL_STORAGE_KEY_SELECTED_DEALER = 'selectedDealerInformation';

export const ApiContext = ({ children }: any) => {
  const [selectedDealer, setselectedDealer] = usePersistState(LOCAL_STORAGE_KEY_SELECTED_DEALER, '1');

  useEffect(() => {
    axios
      .get(`${process.env.GATSBY_API_ENDPOINT}/${selectedDealer}`)
      .then((response) => setselectedDealer(response.data));
  }, [selectedDealer]);

  const changeDealer = (id: any) => {
    setselectedDealer(id);
  };

  localStorage.setItem(LOCAL_STORAGE_KEY_SELECTED_DEALER, JSON.stringify(selectedDealer));

  const { Provider } = context;
  return (
    <Provider value={{ data: selectedDealer, changeDealer: changeDealer }}>{children}</Provider>
  );
};
Related