Array of states within context partially not updating

41 Views Asked by At

So I'm new to Next.js/React and I'm running into a weird state thing and I think it's cause I don't really understand fully what's happening (or it's something super simple and I'm missing something super obvious).

The idea is that I have a player object that has a lot of different components. Each of these components can change individually and so I thought instead of creating one large player object state, I'll create a player object where each key is its own state. Additionally, I want the state to stay on reload and so I need to do some local storage. I tried to make a minimal working example to try and see and I'm getting the same problem.

So I have a context.js file which is basically a wrapper for my context:

'use client'
import { createContext, useContext } from "react";
import { getPlayerStates } from "./defaultPlayerData";

const DataContext = createContext("{}");

export function DataWrapper({children}) {
    let playerStates = getPlayerStates();

    return (
        <DataContext.Provider value={{playerStates}}>
            {children}
        </DataContext.Provider>
    )
}

export function useDataContext() {
    return useContext(DataContext);
}

export function GetPlayerData() {
    const {playerStates} = useDataContext();
    return playerStates;
}

As you can see, I'm using a context provider to create a context which will store the player data. The getPlayerStates function is the one that actually creates the individual states:

import { useLocalStorageNumber } from "./localStorage";

export function GetPlayerStates() {
    const [ener, setEner] = useLocalStorageNumber('energy', 0);
    const [eI, setEI] = useLocalStorageNumber('extraInfo', 0);
    return {
        'energy' : [ener, setEner],
        'extraInfo': [eI, setEi],
        // a lot more properties. Shortened it to make MWE
    }
}

export function updatePlayerData(playerData, playerStates) {
    const playerExists = (playerData && Object.keys(playerData).length > 0)
    if (playerExists) {
        playerStates['energy'][1](playerData.energy);
    }
}

As I said, I need to do everything through local storage because I want to refresh and keep the data. So the local storage file looks like this:

import { useEffect, useState } from "react";

export function useLocalStorageNumber(key, fallbackValue) {
    var fbV = fallbackValue.toString()
    const [value, setValue] = useState(fbV);
    
    useEffect(() => {
        const stored = localStorage.getItem(key);
        if(stored) {
            var x = JSON.parse(stored)
            if (x != value ){
                setValue(x); //
            }
        } else {
            setValue(fbV);
        }   
    }, [fbV, key]);

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value)); //
    }, [key, value]);

    return [value, setValue];
}

So this is where the main state for this individual item will be. So far so good. One last set of functions before I show what I'm outputting. This function is used to grabbing the appropriate keys and adding them into an array which will be returned for the main page.

import { GetPlayerData } from "./context";
import { useLocalStorageNumber } from "./localStorage";

export default function CreateStatesForData(data) {
    const playerStates = GetPlayerData();

    var dataObj = {}
    for (var col of data) {
        for (var k of col) {
            if (k in playerStates) {
                var dataState = playerStates[k]
            }
            dataObj[k] = dataState
        }
    }
    return dataObj
}

Now I make the page.js file to actually display things:

"use client"
import { GetPlayerData} from '@/helpers/context';
import CreateStatesForData from '@/helpers/stateForData';

export default function Page() {
  // Set data required (from playerData)
  var infoRequired = [["energy"]]
  var infoData = CreateStatesForData(infoRequired)

  // Set extra required (not from playerData)
  var extraRequired = [["extraInfo"]]
  var extraData = CreateStatesForData(extraRequired)

  // Helper function - Needed in every isntance (makes code easier to read too)
  function v(key) {
    if (key in infoData) {
      var x = infoData[key][0] 
    } else if (key in extraData) {
      var x = extraData[key][0]
    }
    return x
  }

  var Epow = v("energy")
  

  return (
    <>
        {Epow}
    </>
  )
}

I also have an uploader to actually upload the data:

'use client'
import React, { useRef } from 'react';
import { GetPlayerStates, updatePlayerData } from '@/helpers/defaultPlayerData';

const ImportSaveForm = (props) => {
    let fileReader;

    const inputElem = useRef(null);
    const playerStates = GetPlayerStates();
    
    const handleFileRead = (rawSave) => (e) => {
        var data
        if (rawSave) {
            data = {'energy':100} // not technically how I'm importing, but it's ok.
        } else {
            const content = fileReader.result
            data = JSON.parse(content)
        }

        console.log(data)
        updatePlayerData(data, playerStates);
    }


    const handleFilePick = (e) => {
        var file = e.target.files[0]
        e.target.value = null
        fileReader = new FileReader()
        fileReader.onloadend = handleFileRead(file.type !== 'application/json');
        try {
            fileReader.readAsText(file)
        } catch{
            inputElem.current.value = null
        }

    }

    return (
        <div className="float-right mr-1">
            <input ref={inputElem} style={{ display: "none" }} type='file' id='savefileloader' onChange={e => handleFilePick(e)} />
            <button onClick={() => inputElem.current.click()}>Import save from file</button>
        </div>
    )
}

ImportSaveForm.propTypes = {}
export default ImportSaveForm;

So the idea is that when the user loads the first time, they should have 0 displayed. When they import the variable/state should be updated and the appropriate number (let's pretend it's 100) will be displayed.

The problem: WhenI upload the file, the local storage is appropriately uploading and the useEffect() is running (I can see it in a console.log), but the text itself is not updating which makes me think I'm doing something wrong with the states. (Or maybe in the way I'm passing around the state that makes it not work?)

Any ideas on what I'm doing wrong and/or what I can do to fix it? I'm at a loss what I'm doing wrong here.

Edit: So an additional piece. When I do the following in the return:

  return (
    <>
        {Epow} - {playerData['energy'][0]}
        <button
            onClick={e => 
                playerData['energy'][1](2)
            }
            >Text</button>
    </>
  )
}

and I click the button, it does properly update the display (and the local storage), but on refresh, it again goes back to showing 0. So somehow it's the interaction with the local storage that's causing issues.

Edit 2: It was mentioned by Drew Reese that my hooks are bad so I've gone and run the ES Linter and fixed the hooks, but I'm still getting the same issues. I've edited the code above to reflect the changes I made.

0

There are 0 best solutions below