1

I am developing a tool for running Pen and Paper Campaigns. In this tool I want to be able to display any number of creature entities under one another. They are supposed to be displayed in a certain order.

That order is supposed to be derived from the value of the "ini" of the creatures (i.e. the ones with the highest values get displayed on top). That order is always to be maintained. So when the value changes (since it is contained in an input field) the creature components are supposed to be sorted again.

I have already made the necessary creature components and made a parent component to display them in. But I do not know how to sort them now. I have passed a function from the parent to the children that gets triggered once the value changes. But I do not know how to access the children and sort them based on their state value. I am still quite new to React, so excuse me if this is a trivial thing. I just found no answer anywhere!

I have tried using React.Children.toArray(...) but that does not seem to be right. I also wanted to try out using refs to save the child components. But I did not really understand how to use them and didn't know how to approach this. Beyond that I am rather clueless.

This is my parent component:

export class Encounter extends React.Component<IEncounterProps,IEncounterState> {

    sortByIni(event) {
        //somehow trigger something that sorts the creature components
    }

    addCreature() {

        let creature =
            <Creature
                name={'Ancient Bugbear'}
                //I removed other props for the sake of reducing verbosity... 
                ini={12}
            />;
        return creature
    }


    render(): any {
        return (
            <div>
                {this.addCreature()}
                {this.addCreature()}
                {this.addCreature()}
            </div>

        )
    }

}

And this is the creature component:

export interface ICreatureProps {
    name: string,
    ini: number,
    //Removing non relevant props for verbosity reducing...
}

export interface ICreatureState {
    hitpoints: number
    armorclass: number
    ini: number
}

export class Creature extends React.Component<ICreatureProps, ICreatureState> {

    constructor(props) {
        super(props);
        this.state= {
            hitpoints: this.props.hitpoints || 0,
            armorclass: this.props.armorclass || 0,
            ini: this.props.ini || 0
        };
        this.handleIniChange = this.handleIniChange.bind(this);
        this.handleACChange = this.handleACChange.bind(this);
        this.handleHPChange = this.handleHPChange.bind(this)
    }

    handleIniChange(event) {
        this.setState({ini: event.target.value})
    }

    handleACChange(event) {
        this.setState({armorclass: event.target.value})
    }

    handleHPChange(event) {
        this.setState({hitpoints: event.target.value})
    }

    render(): any {
        return (
            <div>
                <div className={style.creatureContainer}>
                    <div className={style.nextToTitleContainer}>
                        <p className={style.statDisplay}>TP: <input type="number" className={style.inputField} value={this.state.hitpoints} onChange={this.handleHPChange}/></p>
                        <p className={style.statDisplay}>RK: <input type="number" className={style.inputField} value={this.state.armorclass} onChange={this.handleACChange}/></p>
                        //This is the field that is supposed to trigger sorting when it changes value
                        <p className={style.statDisplay}>INI:<input type="number" className={style.inputField} value={this.state.ini} onChange={e=>{this.handleIniChange(e); this.props.sortByIni(e)}}/></p>
                    </div>
                </div>
            </div>
        )
    }
}

This is the supposed outcome when it works. As one sees the creature with the highest value is on top. And of course it is supposed to resort itself whenever a value changes.

Desired Outcome

trashtatur
  • 381
  • 1
  • 3
  • 12

1 Answers1

1

As it always is, soon after you ask, you find the answer yourself:

I solved it by using unique keys each time I render the Creature Components. Since react identifies components via keys, they had to be new each time I rendered due to a change of ini value. If not, react would have not changed the dom tree order, since the component with that key was still there.

I added this function to Encounter to generate creatures based on a button click:

addCreature() {
        let creatureMap = this.state.creatureMap;
        let creatureEntry =
            {
                id: this.uuidv4(),
                name: Math.random().toString(36).substring(7),
                ini: 2,
                currentIni: Math.floor(Math.random() * 20) + 2,

            };
        creatureMap.push(creatureEntry);
        let creatureMapSorted = creatureMap.sort((creatureA, creatureB) => {
            if (creatureA.currentIni < creatureB.currentIni) return 1;
            if (creatureA.currentIni > creatureB.currentIni) return -1;
            return 0;
        });
        this.setState({
            creatureMap: creatureMapSorted
        });
    }

As you can see I gave each creature a unique id. This can most likely also be solved with Refs but I didn't feel fully comfortable with that yet. I am using the uuidv4 Function from this thread (answer from Briguy37). Creatures will give their id when the ini value in their input field changes so it can be changed in the creature map:

<p className={style.statDisplay}>INI:<input type="number" className={style.inputField} defaultValue={this.state.currentIni} onBlur={e=>{this.handleIniChange(e); this.props.sortByIni(e,this.props.id)}}/></p>

which then triggers this function in Encounter

    sortByIni(event,id) {
        let creatureMap = this.state.creatureMap;
        creatureMap.filter(creature=> {
            return creature.id == id
        })[0].currentIni = parseInt(event.target.value);
        let creatureMapSorted = creatureMap.sort((creatureA,creatureB) => {
            if (creatureA.currentIni < creatureB.currentIni) return 1;
            if (creatureA.currentIni > creatureB.currentIni) return -1;
            return 0;
        });
        this.setState({creatureMap : creatureMapSorted})
    }

Creatures objects are put into an array and that array then gets sorted based on current initiative value, before getting applied to the state, thus triggering a render.

As you can see, just a simple sort function with a custom comparator. Nothing fancy. Now as I said, normally react would not care that this list is sorted because the keys for the components are still the same. So it would not rerender them. I solved that problem like that:

    render(): any {

        return (
            <div>
                <button type="button" onClick={this.addCreature}>Add Creature</button>
                {this.state.creatureMap.map((creature, i) => {
                    return (
                        <Creature
                            id={creature.id}
                            name={creature.name}
                            hitpoints={creature.hitpoints}
                            key={this.uuidv4()}
                            */Shortened...*/
                        />
                    )
                })}
            </div>

Like this, the key attribute is unique in every render, thus forcing react to rerender the entire thing and as such keep it sorted!

trashtatur
  • 381
  • 1
  • 3
  • 12