Steve
Steve

Reputation: 125

Why local storage does not work with useEffect?

I have the following code and it is working fine. <AddContact /> is a simple component that presents a input form which collects name + email from user - I have attached its code at the end for completeness. The collected contacts array is stored in localStorage, and when I refresh the page, they simply get reloaded. all good

import './App.css'
import { useState, useEffect } from 'react'

function App() {
    const LOCAL_STORAGE_KEY = 'contacts'

    const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])

    const addNewContact = (newContact) => {
        setContacts([...contacts, newContact])
    }

    useEffect(() => {
        console.table(contacts)
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
    }, [contacts])

    return (
        <AddContact newContact={addNewContact} />
    )
}

export default App

my question is that the following revision does not work - every time the page is refreshed, local storage is wiped out. But it really look like it should work - I was following an online tutorial and it was working when the instructor did it.

import './App.css'
import { useState, useEffect } from 'react'

function App() {
    const LOCAL_STORAGE_KEY = 'contacts'

    const [contacts, setContacts] = useState([]) // changed line

    const addNewContact = (newContact) => {
        setContacts([...contacts, newContact])
    }

    useEffect(() => {
        console.table(contacts)
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
    }, [contacts])

    // added
    useEffect(() => {
        const savedContacts = JSON.parse(
            localStorage.getItem(LOCAL_STORAGE_KEY)
        )
        if (savedContacts) {
            setContacts(savedContacts)
        }
    }, [])


    return (
        <AddContact newContact={addNewContact} />
    )
}

export default App

for completeness, here's the code for <AppContact />

import React, { Component } from 'react'

export class AddContact extends Component {
    state = {
        name: '',
        email: '',
    }

    updateState = (e) => {
        this.setState({ [e.target.name]: e.target.value })
    }

    addContact = (e) => {
        e.preventDefault()
        if (this.state.name === '' || this.state.email === '') {
            return
        }
        this.props.newContact(this.state)
    }

    render() {
        return (
            <div className='ui main'>
                <h2>Add Contact</h2>
                <form className='ui form' onSubmit={this.addContact}>
                    <div className='field'>
                        <label>Name</label>
                        <input
                            type='text'
                            name='name'
                            value={this.state.name}
                            placeholder='Name'
                            onChange={this.updateState}
                        />
                    </div>
                    <div className='field'>
                        <label>Email</label>
                        <input
                            type='text'
                            name='email'
                            value={this.state.email}
                            placeholder='Email'
                            onChange={this.updateState}
                        />
                    </div>
                    <button className='ui button blue' type='submit'>
                        Add
                    </button>
                </form>
            </div>
        )
    }
}

export default AddContact

I would like to understand why the second method does not work.

Upvotes: 2

Views: 5204

Answers (4)

I faced the same issue.

I replaced

useEffect(() => {
        console.table(contacts)
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
    }, [contacts]);

with

 useEffect(() => {
    if (contacts.length > 0) {
      console.table(contacts);
      localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
    }
  }, [contacts]);

This worked for me

Upvotes: 0

Jayesh Jain
Jayesh Jain

Reputation: 1

It is happening because you are reloading and assigning [] to contacts, simply add if condition for empty contacts.

useEffect(()=>{
    if (contacts.length > 0) {
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
    }  },[contacts]);

Upvotes: 0

juliomalves
juliomalves

Reputation: 50268

The value you set when calling addNewContact does get stored in localStorage when the first useEffect runs (as expected). The problem is that, when you reload the page, that same useEffect is overwriting what's in localStorage because the state is reset to an empty array ([]). This triggers the useEffect with contacts equals to [], and stores that in localStorage.


There are a few ways to handle it, but one way is to check if contacts is an empty array before storing its value to localStorage.

useEffect(() => {
    console.table(contacts)
    if (contacts.length > 0) {
        localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
    }
}, [contacts])

This prevents the initial state of contacts to be stored in localStorage when the page gets first loaded.

Upvotes: 7

CertainPerformance
CertainPerformance

Reputation: 370689

In a given render, effect callbacks will run in the order that they're declared. With this:

useEffect(() => {
    console.table(contacts)
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])

// added
useEffect(() => {
    const savedContacts = JSON.parse(
        localStorage.getItem(LOCAL_STORAGE_KEY)
    )
    if (savedContacts) {
        setContacts(savedContacts)
    }
}, [])

On mount, the first one runs before the second - you call localStorage.setItem before the second one runs localStorage.getItem - so by the time the second one runs, storage has been set to the initial value of the contacts state, which is the empty array.

To fix it, reverse their order, so that the one that calls .getItem runs first.

useEffect(() => {
    const savedContacts = JSON.parse(
        localStorage.getItem(LOCAL_STORAGE_KEY)
    )
    if (savedContacts) {
        setContacts(savedContacts)
    }
}, []);
useEffect(() => {
    console.table(contacts)
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts]);

That said, your first approach of

const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])

looks a lot nicer than an effect hook with an empty dependency array, IMO.

Upvotes: 1

Related Questions