McMore
McMore

Reputation: 21

LocalStorage not updating property in state with React hooks

I'm trying to update an object property previously declared in a useState hook for form values and save it in localstorage. Everything goes well, but localstorage is saving date property empty all the time, I know that it must be because of asynchrony but I can't find the solution. This is my code. I'm newbie with React hooks. Lot of thanks!

  const [formValues,setformValues] = useState(
    {
      userName:'',
      tweetText:'',
      date:''
    }
  )

  const getlocalValue = () => {
    const localValue = JSON.parse(localStorage.getItem('tweetList'));
    if(localValue !== null){
      return localValue
    } else {
      return []
    }
  }

  const [tweetList,setTweetList] = useState(getlocalValue());

  const handleInput = (inputName,inputValue) => {
    setformValues((prevFormValues) => {
      return {
        ...prevFormValues,
        [inputName]:inputValue
      }
    })
  }

  const handleForm = () => {
    const {userName,tweetText} = formValues;
    if(!userName || !tweetText) {
      console.log('your tweet is empty');
    } else {
      setformValues(prevFormValues => {
        return {
          ...prevFormValues,
          date:getCurrentDate() //this is not updating in local
        }
      })
      setTweetList(prevTweets => ([...prevTweets, formValues]));
      toggleHidden(!isOpen)
    }

  }
  console.log(formValues) //but you can see changes outside the function


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

Upvotes: 2

Views: 3153

Answers (1)

Zachary Haber
Zachary Haber

Reputation: 11027

In this case the issue is because the handleForm that was called still only has access to the formValues state at the time it was called, rather than the new state. So, the easiest way to handle this is to just update the formValues, setFormValues, and then setTweetList based on the local copy of the updated formValues.

  const handleForm = () => {
    const {userName,tweetText} = formValues;
    if(!userName || !tweetText) {
      console.log('your tweet is empty');
    } else {
      const updatedFormValues = {...formValues,date:getCurrentDate()};
      setformValues(updatedFormValues)
      setTweetList(prevTweets => ([...prevTweets, updatedFormValues]));
      toggleHidden(!isOpen)
    }
  }

Since there's issues with concurrency here: i.e. you can't guarantee an update to the state of both formValues and tweetList with the latest data. Another option is useReducer instead of the two separate state variables because they are related properties and you'd be able to update them based off of each other more easily.

As an example of making more complicated updates with reducers, I added a 'FINALIZE_TWEET' action that will perform both parts of the action at once.

const Component = () => {
  const [{ formValues, tweetList }, dispatch] = useReducer(
    reducer,
    undefined,
    getInitState
  );

  const handleInput = (inputName, inputValue) => {
    dispatch({ type: 'SET_FORM_VALUE', payload: { inputName, inputValue } });
  };

  const handleForm = () => {
    const { userName, tweetText } = formValues;
    if (!userName || !tweetText) {
      console.log('your tweet is empty');
    } else {
      dispatch({ type: 'SET_FORM_DATE' });
      dispatch({ type: 'PUSH_TO_LIST' });
      // OR
      // dispatch({type: 'FINALIZE_TWEET'})
      toggleHidden(!isOpen);
    }
  };
  console.log(formValues); //but you can see changes outside the function

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

  return <div></div>;
};

const getlocalValue = () => {
  const localValue = JSON.parse(localStorage.getItem('tweetList'));
  if (localValue !== null) {
    return localValue;
  } else {
    return [];
  }
};
function getInitState() {
  const initialState = {
    formValues: {
      userName: '',
      tweetText: '',
      date: '',
    },
    tweetList: getlocalValue(),
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'SET_FORM_VALUE':
      return {
        ...state,
        formValues: {
          ...state.formValues,
          [action.payload.inputName]: action.payload.inputValue,
        },
      };
    case 'SET_FORM_DATE':
      return {
        ...state,
        formValues: {
          ...state.formValues,
          date: getCurrentDate(),
        },
      };
    case 'PUSH_TO_LIST':
      return {
        ...state,
        tweetList: [...state.tweetList, state.formValues],
      };
    case 'FINALIZE_TWEET': {
      const newTweet = {
        ...state.formValues,
        date: getCurrentDate(),
      };
      return {
        ...state,
        formValues: newTweet,
        tweetList: [...state.tweetList, newTweet],
      };
    }
    default:
      return state;
  }
}

Upvotes: 2

Related Questions