Chris Wu
Chris Wu

Reputation: 43

How to persist state using localstorage with NextJS, ContextAPI, and useReducer

I'm creating a job site where you can add specific job cards to a favorites section by using the ContextAPI and useReducer hook for global state management.

I followed the code for using contextAPI and useReducer through this video on youtube: https://www.youtube.com/watch?v=awGFsGc9oCM&t=946s, attached is also the sandbox code for it https://codesandbox.io/s/usereducer-hook-swkwl

The favoriting function now works, however the next step I'd like to achieve is to persist those favorited jobs upon page refresh using localStorage.

I found this stackoverflow site Persist localStorage with useReducer showing how to initialize state with localStorage, but I'm having some issues incorporating the implementation, specifically that in my application, you can see how when each job is added to favorites, it is also added in localstorage, however the content of the localstorage resets to an empty array after page refresh. I'm missing something but I'm not sure where the problem is.

JobCard.js

import { useState, useEffect } from "react";
import useJob from "../context/JobContext";
const JobCard = ({id,title,responsibilities,job,locations,updated_date,url}) => {

  const {jobArray, addToFavorite, removeFavorite} = useJob();

  const [isInFav,setIsInFav] = useState(false);

  useEffect(() => {
  
    const jobIsInFavorite = jobArray.find((job) => job.id === id);
    
    if (jobIsInFavorite) {
      setIsInFav(true);
    } else {
      setIsInFav(false);
    }
  }, [jobArray, id]);

  const handleClick = () => {
    const singleJob = {id,title,responsibilities,job,locations,updated_date,url}
    
    if (isInFav) {
      removeFavorite(singleJob);
    } else {  
      addToFavorite(singleJob);
    }
  };

  return (
    <div>
    <div className="container" key = {id}>
      <div className="card">

        <div className="row ">
          <div className="card-title col-sm-6">
             <p className="heading">{title}</p>
             <p className="col-4 col-md-5"><img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRk4fgpQ_EFjQ96BAtFlYIPIjfQdcrWsRzwzQ&usqp=CAU" className="icon2"></img> {job.company.name}</p>
             <p className="text-muted">{responsibilities}</p>

             <span className="col-4 col-md-4"><img src="https://toppng.com/uploads/preview/location-pin-comments-location-icon-small-11562928969ztngidtv1e.png" className="icon"></img> {locations != 0 ? locations[0].value : null}</span>

          </div>

          <div className="job-info col-sm-4">
            <p><strong>Job Type:</strong>  {job.type == 1 ? "Intern" : ""} </p>
            {/* isn't updated different from posted date? we should put posted date if possible*/}
            <p><strong>Updated:</strong>  {updated_date.substring(0,10)} </p>
          </div>


          <div className="apply-btn col-sm">
          <button type="button" className="btn btn-outline-primary btn-md float-right"><a href={url}> Apply Now </a></button>
          <button type="button" className="btn btn-outline-primary btn-md float-right" onClick={handleClick} isInFav={isInFav}><p>{isInFav ? "-" : "+"}</p></button>
          </div>

        </div>
      </div>
    </div>
  
    </div>
    )
}
  export default JobCard;

JobContext.js

import { createContext, useReducer, useContext,useEffect } from "react";
import jobReducer, { initialState,initializer } from "./JobReducer";

const JobContext = createContext(initialState)

export const JobProvider = ({children}) => {
    const [state,dispatch] = useReducer(jobReducer,initialState,initializer);

    useEffect(() => {
        localStorage.setItem("favorited-jobs", JSON.stringify(state));
      }, [state]);

    const addToFavorite = (job) => {
        const updatedJobs = state.jobArray.concat(job)
        
        dispatch({
            type: "ADD_TO_FAVORITE",
            payload:{
                jobArray:updatedJobs
            }
        })
    }

    const removeFavorite = (job) => {
        const updatedJobs = state.jobArray.filter((currentJob) => 
            currentJob.id !== job.id)

        dispatch({
            type: "REMOVE_FROM_FAVORITE",
            payload:{
                jobArray:updatedJobs
            }
        })
    }

    const value = {
        jobArray: state.jobArray,
        addToFavorite, 
        removeFavorite,
    }

    return <JobContext.Provider value={value}>{children}</JobContext.Provider>
}
//custom hook
const useJob = () => {
    const context = useContext(JobContext)

    if(context === undefined){
        throw new Error("Error")
    }
    return context
}

export default useJob 

jobReducer.js

export const initialState = {
    jobArray: []
}

export const initializer = (initialValue = initialState.jobArray) =>
  JSON.parse(localStorage.getItem("favorited-jobs")) || initialValue;

const jobReducer = (state,action) => {
    const {type,payload} = action;

    switch(type) {
        case "ADD_TO_FAVORITE":
            console.log("ADD_TO_FAVORITE",payload)
            return {
                ...state,
                jobArray:payload.jobArray
            }
        case "REMOVE_FROM_FAVORITE":
            console.log("REMOVE_FROM_FAVORITE",payload)
            return {
                ...state,
                jobArray:payload.jobArray
            }
        default:
            throw new Error("Nothing ")
    }
}

export default jobReducer

Favorites.js

import { useEffect, useState } from "react";
import JobCard from "../components/JobCard";
import useJob from "../context/JobContext";

const Favorites = () => {
  const {jobArray} = useJob();
  return(
      <div className="favorite_list">
        {jobArray.map((job, index) => (
          
          <JobCard key={index} {...job} />
        ))}
    </div>
  )
}

export default Favorites

Upvotes: 2

Views: 1849

Answers (1)

Enfield Li
Enfield Li

Reputation: 2530

Try this:

useEffect(() => {
  if(state.jobArray.length) { // <- make sure array is not empty
    localStorage.setItem("favorited-jobs", JSON.stringify(state));
  }
}, [state]);

Everytime page refresh, state is initialized to empty array, and local storage will pick up the initial state, override previous value. By doing a empty check, the localstorage data won't be overridden. BTW, it's generally a good idea to do a empty check before updating array.

Sandbox to test out useEffect approach

On a second look, you acctually don't have to rely on useEffect to update localStorage, a better way would be:

// delete useEffect first

    const addToFavorite = (job) => {
        const updatedJobs = state.jobArray.concat(job)
        localStorage.setItem("favorited-jobs", JSON.stringify(updatedJobs )); // update directly

        dispatch({
            type: "ADD_TO_FAVORITE",
            payload:{
                jobArray:updatedJobs
            }
        })
    }

    const removeFavorite = (job) => {
        const updatedJobs = state.jobArray.filter((currentJob) => 
            currentJob.id !== job.id)
        localStorage.setItem("favorited-jobs", JSON.stringify(updatedJobs )); // update directly

        dispatch({
            type: "REMOVE_FROM_FAVORITE",
            payload:{
                jobArray:updatedJobs
            }
        })
    }

Upvotes: 1

Related Questions