#[React/Typescript] Passing reference of an instance of an object in the parent component to child.

18 messages · Page 1 of 1 (latest)

neon kindle
#

Hello, I have a question about creating objects and passing them as reference down to children components as props.

I have the following code pieces:

  const playerStatController = new PlayerStatController(playerStats,(value)=>{
    console.log("Should update DOM: ",value);
    setPlayerStats(value)
  });// Instances a class that controls player stats state
const cardGenerator = new CardGenerator(playerStatController);// Instances a class that creates cards
  const [currentDeck,setCurrentDeck] = useState<Card[]>(cardGenerator.GenerateCards(INITIAL_DECK)); // Using the card generator object, Generate Cards method will create objs based on passed ID's and inject them with the stat controller

The idea is that I have an object responsible for handling the state, which works if I directly access its methods with a test function, as the DOM updates with expected values.

Every "card" object has access to the stat handling object, as a reference is passed upon instancing.

They all could access the stat handling object and perform DOM updates, however there would be weird interactions with multiple cards trying to update the DOM at the same time with different values... therefore I made an accumulator method inside the stat handling object, that will simply go through all the cards and add their values, then dump the result in the stat.

And that's where things started happening weirdly.

#

I have a button to test the interaction between each component:

<button onClick={()=>{
        currentActiveStack.forEach(activeCard => {
          activeCard.Countdown();
          console.log("After each card activates",playerStatController._statChangeAccumulator);
        });
        playerStatController.UnloadAccumulators();
        console.log("Done counting down");
      }}>Countdown</button>

This will cycle through each card in the array, access the "Countdown" method:

Countdown() : void {
        // Children may override this method to add "On Countdown" effects
        if(this._remainingDuration<=0){
            this.Exhaust();
        } else {
            this.Active();
        }
        this._remainingDuration-=this.DrainRate; // Reduces remaining duration by the drain rate
    }

Each countdown method will call the "Active" method:

Active(): void {
        // This is the cards basic functionality while active
        this.AddStat("Attack",this.Attack);
        this.AddStat("Defense",this.Defense);
    }
#

and therefore it will call the "AddStat" Method:

AddStat(statName: string, statValue : number):void{
        const accValue = this.playerStatController.getStatChangeAccumulator(statName);
        const newPair = {name: statName, value: accValue+statValue}
        this.playerStatController.setStatChangeAccumulator(newPair);
    }

It does a simple add, based on the current accumulated value and the card's stats. Then calls its instance of the stat handling object to change the accumulator value:

getStatChangeAccumulator(name: string) : number { // Method that returns the current value of an accumulator, given its unique name as a parameter
        const statAccIndex = this._statChangeAccumulator.findIndex(accumulator=> accumulator.name === name);
        if(statAccIndex!=-1){
            return this._statChangeAccumulator[statAccIndex].value; // Already exists, return current value
        } else {
            return 0; // Doesn't yet exist, so returns 0
        }
    }

    setStatChangeAccumulator(v: {name: string, value: number}) { // This property has a public setter to change the value of a specific accumulator, based on a given {key,value} pair
        const statAccIndex = this._statChangeAccumulator.findIndex(accumulator => accumulator.name === v.name)
        if(statAccIndex != -1){
            console.log("Acc exists and is updating",statAccIndex);
            this._statChangeAccumulator[statAccIndex].value = v.value
            console.log(this._statChangeAccumulator);
        } else {
            console.log("Acc doesn't exist, new one is being pushed");
            this._statChangeAccumulator.push(v);
            console.log(this._statChangeAccumulator);
        }
    }

#

After each card does its calculations, the app will then unload the accumulators into the stats:

 UnloadAccumulators():void{// This method is called to update all stats after "Accumulation" is over
        console.log(this._statChangeAccumulator);
        this._statChangeAccumulator.forEach(accumulator => {
            console.log(accumulator);
            const [success,statValue] = this.getStat(accumulator.name);// Tries to access a stat with a name that matches the accumulator's name, if that fails then the method returns false and the value is ignored
            if(success)
            {
                const newStat = {name: accumulator.name, value: statValue}// Assembles a new object with the updated stat value
                this.setStat(newStat);// Go through each accumulator and add their values to the stat
            }  
        });
        this.resetAccumulator();
    }
#

What is confusing me is that I have a console.log() inside the setStatChangeAccumulator method that logs the current values of what is being stored. And they are being called correctly, calculations are happening as expected.

However, when the time comes to "Unload" the accumulators into the stats, they are empty. The console.log() located at the UnloadAccumulators Method is printing an empty array, whilst it was populated with the proper accumulators.

Console.log() on the parent component after each card cycle will also return an empty array, right after the card did its calculations and just printed a populated array.

Is my code creating separate instances of the PlayerStatController object? It seems to be behaving as such, but I don't understand why.

clear gale
#

@neon kindle If you do:

const [playerStats,setPlayerStats] = useState<PlayerStat[]>(INITIAL_PLAYER_STATS);
const playerStatController = new PlayerStatController(playerStats,(value)=>{

Then, yes, you're making a new PlayerStatsController on every render.

#

I would strongly recommend not trying to control your react state via classes.

#

React is based on a functional/immutable model, not an OOP model, so the patterns you should use to organize code are different than what you might be used to from OOP

neon kindle
#

That's right :<, I see that. After the first render, then the objects in the cards that are kept as state are therefore different than the instance I have

#

@clear gale is there a reason I shouldn't control my react state via classes though? Isn't it inherently the same, although encapsulated in the object?

clear gale
#

Classes tend to operate on mutation - if you're careful to never do that and always interact directly with the underlying React values and setters and the class is functionally just a bunch of pure functions attached into one object, you could do it that way, but I would still not recommend it.

#

The class just isn't adding much at that point and there's a lot of more idiomatic ways to structure React code that would be better.

neon kindle
#

Thanks! Do you have any tips on what I should look for? I'm currently thinking the least painful way to approach this is to pass the class that has been assembled after the render (using react state) to the cards whenever they are played, so that they should now be synced with the rest of the app. I'll be testing this out in a few minutes, but I see what you mean that it loses some of its purpose, now the class is just a blob of functions.

clear gale
#

I often recommend starting with fairly vanilla react state usage; you can get a lot farther with that than most people think.

A fairly popular pattern beyond that is a reducer pattern, where you have the state and a dispatch function where you can trigger specific events to change the state. Either useReducer which is built into React, or redux (a global version, which has easier support for stuff like async actions)

neon kindle
#

Okay! I'll look into those :), thanks for the help! Currently the code is working just by passing the object to every card, but I'll see if I can improve on it with proper react usage.

clear gale
#

If you're actually doing some sort of complex game, I've also seen recommendations to separate out the game logic entirely and just have React be a pure-view layer; there's probably merit to that from a performance/technical perspective, but it's not very helpful if the goal is to learn React.

neon kindle
#

The goal is indeed to learn react through a small game hahah, shouldn't be too complex!

#
<button onClick={()=>{
        currentActiveStack.forEach(activeCard => {
          activeCard.Countdown(playerStatController);
          console.log("After each card activates",playerStatController._statChangeAccumulator);
        });
        playerStatController.UnloadAccumulators();
        console.log("Done counting down");
      }}>Countdown</button>