Search code examples
javascriptreactjsreact-router-dom

Route with param showing the old state when changing the param


I have category pages for Drinks and Foods that use the same component, CategoryPage. Their URLs are /category/food and /category/drinks; when I'm moving from Drinks to route other than Foods or vice versa, it keeps the same state value as it was on the previous category route, but I need it to reset as it was while assigning its value.

The main idea is to show a spinner while item pictures are still loading; if this is not the correct way to do so, I would be glad if you showed me the correct way to solve this problem.

General Category page code:

const CategoryPage = () =>{
    const { name } = useParams()
    const loading = useSelector(state => state.shop.loading)
    const categoryItems = useSelector(state => state.shop.categoryItems)
    const dispatch = useDispatch()
    //counting loaded images that should be shown in category page
    const [loadedImages,setLoadedImages] = useState(0)

    useEffect(() => {
        dispatch(getShopData(`categories/${name}/products`,'category'))
    },[dispatch,name])

    useEffect(() =>{
        //when url params change, it means the user moved to the new category page 
        // so loadedImagesState must be reset to 0 with useffect cause without it, the state will remain the same
        // as it was at the end of previous category page 
        setLoadedImages(0)
    },[name])

    return(
        <>
            <img src={bg} className={classes["bg"]}/>
            {categoryItems.length > 0 && 
                <div className={classes["container"]}>
                    {categoryItems.map((product,id) =>
                        <CategoryItem key={id} product={product} setLoadedImages={setLoadedImages} /> 
                    )}
                </div>
            }
            {(loading || loadedImages != categoryItems.length) &&
                <div className="page-load-wrapper">
                    <Loader manualStyles={{width:"75px",height:"75px"}} />
                </div>
            }
        </>
    )
}

CategoryItem code:

const CategoryItem = ({ product, stylesFor="", setLoadedImages}) =>{
    const dispatch = useDispatch()

    const addItem = () =>{
        dispatch(cartActions.addItem(product))
    }

    const notifyImageLoad = () =>{
        if(setLoadedImages){
            setLoadedImages(prev => prev +1)
        }
    }

    return(
        <div className={[classes["product-item"], classes[stylesFor]].join(' ')}>
            <img className={[classes["product-img"], classes[stylesFor]].join(' ')} src={product.img} onLoad={notifyImageLoad} />
            <div className={[classes["product-desc-container"], classes[stylesFor]].join(' ')}>
                <h2 className={[classes["product-title"], classes[stylesFor]].join(' ')}>{product.title}</h2>
                <h2 className={[classes["product-desc"], classes[stylesFor]].join(' ')}>Something about product</h2>
                <h2 className={[classes["product-price"], classes[stylesFor]].join(' ')}>Price: {product.price}$</h2>
                <button onClick={addItem} className={[classes["add-button"], classes[stylesFor]].join(' ')}>ADD TO CART</button>
            </div>
        </div>
    )
}

Routes in the App:

const App = () => {
  return (
    <>
      <Header />

      <Routes>
        <Route path="/" element={<Navigate to="/home" />} />
        <Route path="/home" element={<HomePage />} />
        <Route path="/category/:name" element={<CategoryPage />} />
        <Route path="/cart" element={<CartPage />} />
        <Route path="*" element={<h1>Error 404 page not found</h1>} />
      </Routes>
    </>
  );
}

I used name const from useParams to catch when the route param changes so I can reset the state value from useEffect as shown there, but when I move from /category/:nameX to the non-category page and then /category/:nameY, the state still remains same as it was at /category/:nameX.


Solution

  • To fix your problem, you need a clean-up function in your useEffect where you are calling dispatch, to reset your state/store before the component gets unmounted:

    const CategoryPage = () => {
      const { name } = useParams();
      const loading = useSelector((state) => state.shop.loading);
      const categoryItems = useSelector((state) => state.shop.categoryItems);
      const dispatch = useDispatch();
      //counting loaded images that should be shown in category page
      const [loadedImages, setLoadedImages] = useState(0);
    
      useEffect(() => {
        dispatch(getShopData(`categories/${name}/products`, "category"));
    
        // Here is your clean-up function 👈🏽
        return () => {
          dispatch(/* set shop.categoryItems to an empty array */);
          dispatch(/* set shop.loading to true*/);
          setLoadedImages(0);
        };
      }, [dispatch, name]);
    
      return (
        <>
          <img src={bg} className={classes["bg"]} />
          {categoryItems.length > 0 && (
            <div className={classes["container"]}>
              {categoryItems.map((product, id) => (
                <CategoryItem key={id} product={product} setLoadedImages={setLoadedImages} />
              ))}
            </div>
          )}
          {(loading || loadedImages != categoryItems.length) && (
            <div className="page-load-wrapper">
              <Loader manualStyles={{ width: "75px", height: "75px" }} />
            </div>
          )}
        </>
      );
    };