Home > other >  Infinite loop with useEffect hook with useSelector
Infinite loop with useEffect hook with useSelector

Time:10-25

I have a normalized Redux store in my React Native application.

The structure of my reducer is:

{
  byId: {},
  allIds: []
}

In my component, I get the slice of Redux state using the useSelector hook:

const categories = useSelector((state: AppState) =>
  state.products.allIds.map((id) => state.categories.byId[id.toString()])
);

The logic in the useSelector just converts the byId object into an array.

The infinite looping occurs when I set the categories array as a dependency:

const [values, setValues] = useState<any[]>([]);

useEffect(() => {
  setValues([{ id: 1 }]);
  console.log("logging");
}, [categories]);

Not sure what is the problem. I believe it's the useSelector logic that converts the objects into an array.

EDIT:

Full component code:

// React
import React, { useEffect, useState } from "react";

// React redux
import { useSelector } from "react-redux";
import { AppState } from "@reducers/rootReducer";

// Logic
import ProductsScreenLogic from "./ProductsScreen.logic";

// Common components
import ScreenView from "@common/screen/Screen.view";

// Components
import NewProductModalView from "@components/products/new-product-modal/NewProductModal.view";
import ProductsTabsView from "@components/products/products-tabs/ProductsTabs.view";
import ProductListView from "@components/products/products-list/ProductList.view";
import CategoryListView from "@components/products/category-list/CategoryList.view";

const ProductsScreenView: React.FC = () => {
  const { displayProductList, setDisplayProductList, products } =
    ProductsScreenLogic();


  // Makes the categories ById object into an array of categories
  const categories = useSelector((state: AppState) => state.categories.allIds.map((id) => state.categories.byId[id.toString()])
  );


  const [values, setValues] = useState<any[]>([]);

  useEffect(() => {
    setValues([{ id: 1 }]);
    console.log("logging");
  }, [categories]);

  return (
    <>
      <NewProductModalView />
      <ScreenView></ScreenView>
    </>
  );
};

export default ProductsScreenView;

CodePudding user response:

In useEffect you update the state , wich cause render , render call useSelector wich every time return new array for useEffect , wich cause update the state. To fix you can remove categories from useEffect dependency array

CodePudding user response:

The problem is that your selector always returns a new reference (because of the call to map). You could instead use createSelector which will memoize it and only return a new reference when something inside either allIds or byId changes:

const selectAllCategories = createSelector(
    (state: AppState) => state.categories.allIds,
    (state: AppState) => state.categories.byId,
    (categoriesIds, categoriesById) => categoriesIds.map((id) => categoriesById[id.toString()])
);

But ideally you should avoid such selectors that go through the whole byId object, because it kind of negates the benefits of having a normalized state. You should rather have a parent component that only selects state.categories.allIds, and then passes the ids as props to child components, and every child component will select its own state.categories.byId[id]. This way if a category changes, only the corresponding child component will rerender, instead of having the parent and all of the children rerender.

  • Related