Search code examples
javascriptreactjstypescriptreduxredux-toolkit

How do I avoid flash of an unnecessary component for 1 second after fetching data in React?


enter image description here

As you can see on the image, after fetching data and displaying it on the screen, orange button appears (in the center) for 1 second and then dissappears.

Code for component:

const Home: FC = () => {
    const { pizzas, loading, error, count } = useAppSelector(
        (state) => state.pizzas
    )
    const { categoryID, searchValue, currentPage, sortNameObj } =
        useAppSelector((state) => state.filter)
    const dispatch = useAppDispatch()

    const handleChangeCategory = useCallback((index: number) => {
        dispatch(setCategoryID(index))
        dispatch(setCurrentPage(1))
    }, [])

    const handleChangePage = (page: number) => {
        dispatch(setCurrentPage(page))
    }

    const pizzaList = pizzas?.map((item) => {
        const pizzaImg = pizzaImagesMiddle[item.title]

        return <PizzaCard key={item.id} item={item} pizzaImg={pizzaImg} />
    })

    const skeletons = [...new Array(4)].map((_, index) => (
        <PizzaSkeleton key={index} />
    ))

    const loadedPizzas = loading ? skeletons : pizzaList

    useEffect(() => {
        dispatch(fetchPizzas())
    }, [categoryID, searchValue, sortNameObj, currentPage])

    if (error) {
        return <EmptyResult title='Произошла ошибка' />
    }

    if (!loading && (!pizzas || pizzas?.length === 0)) {
        return <EmptyResult title='Пиццы не найдены' />
    }

    return (
        <div className={styles.home__container}>
            <div className={styles.content__header}>
                <Categories
                    handleChangeCategory={handleChangeCategory}
                    value={categoryID}
                />
                <Sort sortNameObj={sortNameObj} />
            </div>
            <h2>Все пиццы</h2>
            <section className={styles.content__list}>{loadedPizzas}</section>
            <Pagination
                handleChangePage={handleChangePage}
                currentPage={currentPage}
                itemsLength={count}
            />
        </div>
    )
}

It is because cheking pizzas length in condition if (!loading && (!pizzas || pizzas?.length === 0)). Without check of empty length if (!loading && !pizzas) everything goes well. But I need to check if array is empty or not.

In default, pizzas length is empty (so I have empty array before fetching data)

Pizza slice:

const initialState: PizzasState = {
    pizzas: [],
    loading: false,
    error: null,
    count: 0
}

const pizzasSlice = createSlice({
    name: 'pizzas',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder.addCase(fetchPizzas.pending, (state) => {
            state.loading = true;
            state.pizzas = [];
            state.error = null;
            state.count = 0
        });
        builder.addCase(fetchPizzas.fulfilled, (state, action) => {
            state.pizzas = action.payload.items;
            state.error = null;
            state.count = action.payload.count;
            state.loading = false
        });
        builder.addCase(fetchPizzas.rejected, (state, action) => {
            state.pizzas = [];
            state.count = 0;
            if (action.payload) {
                state.error = action.payload.message
            } else {
                state.error = action.error.message
            };
            state.loading = false
        })
    }
})

Question: How to properly avoid flashing <EmptyResult/> for 1 second?


Solution

  • Issue

    The EmptyResult component is currently displayed when loading is true and the pizzas state is falsey or empty. The pizzas state is initially empty and is also set to [] when the fetchPizzas action is pending.

    Solution

    If you only want to display EmptyResult once data has been loaded then select a different value from [] - "fetched data and empty" and [....] - "fetched data and populated" to differentiate the states and loading conditions. Using undefined or null are valid choices here to indicate that data hasn't been fetched/loaded yet, and is easy to check in the UI.

    const initialState: PizzasState = {
      pizzas: undefined, // <-- initially undefined
      loading: false,
      error: null,
      count: 0,
    };
    
    const pizzasSlice = createSlice({
      name: 'pizzas',
      initialState,
      extraReducers: (builder) => {
        builder.addCase(fetchPizzas.pending, (state) => {
          state.loading = true;
          // state.pizzas = []; // <-- Don't update yet
          state.error = null;
          state.count = 0
        });
        builder.addCase(fetchPizzas.fulfilled, (state, action) => {
          state.pizzas = action.payload.items; // <-- update to "loaded" value
          state.error = null;
          state.count = action.payload.count;
          state.loading = false;
        });
        builder.addCase(fetchPizzas.rejected, (state, action) => {
          state.pizzas = []; // <-- update to "loaded" value
          state.count = 0;
          state.error = action.payload
            ? action.payload.message
            : action.error.message;
          state.loading = false;
        });
      },
    });
    

    Update the UI to check for undefined/null loaded data.

    const skeletons = [...new Array(4)].map((_, index) => (
      <PizzaSkeleton key={index} />
    ));
    
    const Home: FC = () => {
      const { pizzas, loading, error, count } =
        useAppSelector((state) => state.pizzas);
      const { categoryID, searchValue, currentPage, sortNameObj } =
        useAppSelector((state) => state.filter);
      const dispatch = useAppDispatch();
    
      const handleChangeCategory = useCallback((index: number) => {
        dispatch(setCategoryID(index));
        dispatch(setCurrentPage(1));
      }, []);
    
      useEffect(() => {
        dispatch(fetchPizzas())
      }, [categoryID, searchValue, sortNameObj, currentPage])
    
      const handleChangePage = (page: number) => {
        dispatch(setCurrentPage(page));
      };
    
      if (error) {
        return <EmptyResult title='Произошла ошибка' />;
      }
    
      // Check if pizzas is a defined array and empty
      if (!loading && (Array.isArray(pizzas) && !pizzas.length)) {
        return <EmptyResult title='Пиццы не найдены' />;
      }
    
      return (
        <div className={styles.home__container}>
          <div className={styles.content__header}>
            <Categories
              handleChangeCategory={handleChangeCategory}
              value={categoryID}
            />
            <Sort sortNameObj={sortNameObj} />
          </div>
          <h2>Все пиццы</h2>
          <section className={styles.content__list}>
            {loading
              ? skeletons
              : pizzas?.map((item) => (
                <PizzaCard
                  key={item.id}
                  item={item}
                  pizzaImg={pizzaImagesMiddle[item.title]}
                />
              ))
            }
          </section>
          <Pagination
            handleChangePage={handleChangePage}
            currentPage={currentPage}
            itemsLength={count}
          />
        </div>
      );
    };