AJSwift
AJSwift

Reputation: 719

Converting componentDidMount into useEffect

I am trying to learn React hooks, and trying to convert existing codebase to use hooks, but I am confused. Is it normal to set state inside useEffect? Would I be causing the dreaded infinite loop if I do so?

import React, { useState, useEffect } from 'react';
import App from 'next/app';
import Layout from './../components/Layout';

function MyApp({ Component, pageProps }) {
    const [ cart, setCart ] = useState([]);

    const addToCart = (product) => {
        setCart((prevCartState) => {
            return [ ...prevCartState, product ];
        });
        localStorage.setItem('cart', JSON.stringify(cart));
    };

    //mimicking componentDidMount to run some effect using useEffect
    //useEffect(() => setCount((currentCount) => currentCount + 1), []);
    useEffect(() => {
        const cartCache = JSON.parse(localStorage.getItem('cart'));
        //cart = cartCache; Good or bad?
        cartCache || setCart(() =>{

        });        
    }, []);
    return <Component {...pageProps} />;
}

My original class based component:

/*
export default class MyApp extends App {
  state = {
    cart: []
  }
  componentDidMount = () => {
    const cart = JSON.parse(localStorage.getItem('cart'));
    if (cart) {
      this.setState({
        cart
      });
    }
  };
  addToCart = (product) => {
    this.setState({
      cart: [...this.state.cart, product]
    });
    localStorage.setItem('cart', JSON.stringify(this.state.cart));
  }
  render() {
    const { Component, pageProps } = this.props
    return (
      <contextCart.Provider value={{ cart: this.state.cart, addToCart: this.addToCart }}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </contextCart.Provider>
    )
  }
}*/

Upvotes: 3

Views: 2831

Answers (2)

norbitrial
norbitrial

Reputation: 15176

Because localStorage.getItem() is a synchronous call thus for this scenario you can also use the function callback version of useState in order to set initial value. In this way you don't need to use useEffect. Usually if it is possible I try to avoid introducing any new side effects in my functional components.

You can try as the following instead:

const [ cart, setCart ] = useState(() => {
   const cartCache = JSON.parse(localStorage.getItem('cart'));

   if (cartCache) {
      return cartCache;
   }

   localStorage.setItem('cart', JSON.stringify([]));
   return [];
});

In this way if the cart element is missing from the localStorage the code will create it with a default [] empty array and set it also for your state. Other case it will set your state the value from storage.

Please note: Also I agree with the answer from kind user here in terms of listening cart state changes to keep localStorage up to date with useEffect. My suggestion is only for the initial state.

Upvotes: 2

kind user
kind user

Reputation: 41913

It's okey to set state inside useEffect as long as you don't listen to changes of the same field inside dependency array. In your particular case you are calling useEffect only once (since you have passed an empty dependency array).

useEffect(() => {
   const cartCache = JSON.parse(localStorage.getItem('cart'));
   if (cartCache) {
      setCart(cartCache);   
   }     
}, []);

Also would be cool to add the second useEffect to listen to cart changes and keep the localStorage up-to-date.

useEffect(() => {
   localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);

Upvotes: 3

Related Questions