Search code examples
reactjsreduxuse-effect

Handle React render / flickering whilst awaiting useEffect async operaion


I have the below code which works really well, but I can't see to handle the fact that 'NOTHING' is displayed with a flicker as the state changes / there is re-rendering before the data is shown.

How can I wait until the state is updated and only show the spinning loader until all the hooks have executed / the final state is recognized final render have taken place?

A note that item.something is originally undefined before useEffect hence the need to check before returning.

Does there need to be an initial state set (so that item.something !== undefined, and if so, how would I do that?

Is it possibly something like const [item, setItem] = useState('Somehting here')?

import { useEffect, useState } from 'react';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { getItem } from '../features/itemSlice';

import { toast } from 'react-toastify';

import Spinner from '../components/Spinner';


function Items() {
  const [searchParams, setSearchParams] = useSearchParams();
  const itemObject = searchParams.get(`item`);
  

  const { item, isLoading, isError, message } = useSelector((state) => state.task);

  const dispatch = useDispatch();

 
  useEffect(() => {
    if (isError) {
      toast.error(message);
    }

    dispatch(getItem(itemObject));
    // eslint-disable-next-line
  }, [isError, message, itemObject]);

  if (isLoading) {
    return <Spinner />;
  }

  if (isError) {
    return <h3>Error</h3>;
  }

  return (
    <>
      <div>
        {!item.something ? ( //How to refactor here(?) so that nothing is rendered if no data and await result of useEffect hook
          <p>NOTHING</p>
        ) : (
          item.something.map((element, index) => {
            return element.body ? <p key={index}>{element.somethingElse}</p> : <p key={index}>Truly Nothing This Time</p>;
          })
        )}
      </div>
    </>
  );
}

export default Items;

Solution

  • Should be able to accomplish this by adding another condition in your spinner return that check that something is available.

    import { useEffect, useState } from 'react';
    import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
    import { useSelector, useDispatch } from 'react-redux';
    import { getItem } from '../features/itemSlice';
    
    import { toast } from 'react-toastify';
    
    import Spinner from '../components/Spinner';
    
    
    function Items() {
      const [searchParams, setSearchParams] = useSearchParams();
      const itemObject = searchParams.get(`item`);
      
    
      const { item, isLoading, isError, message } = useSelector((state) => state.task);
    
      const dispatch = useDispatch();
    
     
      useEffect(() => {
        if (isError) {
          toast.error(message);
        }
    
        dispatch(getItem(itemObject));
        // eslint-disable-next-line
      }, [isError, message, itemObject]);
    
      // move this to the top because in error state `item.something` will likely be falsy
      if (isError) {
        return <h3>Error</h3>;
      }
    
      if (isLoading || !item.something) {  // renders spinner when `item.something` is undefined (or any falsy value)
        return <Spinner />;
      }
    
      return (
        <>
          <div>
            {item.something.map((element, index) => {
                return element.body ? <p key={index}>{element.somethingElse}</p> : <p key={index}>Truly Nothing This Time</p>;
              })
            )}
          </div>
        </>
      );
    }