Access children properties (state?) from parent

I’m building my first React app, and I’m a little confused in how to talk between components.

The app calculates the score of the player of certain game. For this, I create an array of components and then I need to sum the scores of each in the parent component.

The code of each component is the following:

Parent:

const RouteScore = () => {
    const routes = [
        { length: 1, score: 1 },
        { length: 2, score: 2 },
        { length: 3, score: 4 },
        { length: 4, score: 7 },
        { length: 6, score: 15 },
        { length: 8, score: 21 },
    ]

    const buttons = routes.map((route) => <RouteScoreButton length={route.length} score={route.score} key={route.length} />)

    return (
        <div className="position-absolute top-50 start-50 translate-middle">
            <div className="bg-light border border-primary rounded-3 px-4 py-4 opacity-75 text-center">
                <table className="table">
                    <thead>
                        <tr className="text-center">
                            <th scope="col">Route length</th>
                            <th scope="col">Total routes</th>
                            <th scope="col">Total score</th>
                        </tr>
                    </thead>
                    <tbody className="text-center align-middle">
                        {buttons}
                    </tbody>
                </table>
                <h3>Total score:</h3>
            </div>
        </div>
    )
}

Children:

const RouteScoreButton = (props) => {
    const [numberRoutes, setNumberRoutes] = useState(0)
    const score = useMemo(() => props.score * numberRoutes, [numberRoutes])

    const increaseRoute = () => {
        setNumberRoutes(numberRoutes + 1)
    }
    const decreaseRoute = () => {
        numberRoutes > 0 && setNumberRoutes(numberRoutes - 1)
    }

    const handleChange = (e) => {
        let newValue = parseInt(e.target.value)
        if (isNaN(newValue)) {
            setNumberRoutes(0)
        } else {
            setNumberRoutes(parseInt(e.target.value))
        }
    }

    return (
        <tr>
            <th>
                <h3>{props.length}</h3>
            </th>
            <th>
                <div className="input-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-primary" onClick={increaseRoute}>+</button>
                    <input type="text" className="form-control text-center" style={{ width: '50px' }} value={numberRoutes} onChange={handleChange} />
                    <button type="button" className="btn btn-primary" onClick={decreaseRoute}>-</button>
                </div>
            </th>
            <th>
                <h3>{score}</h3>
            </th>
        </tr>
    )
}

How can I calculate the sum of scores of every child component?

Answer

In this case its better to move the state one level up, So in parent it would be

const RouteScore = () => {
    const [routes, setRoutes] = useState([
        { length: 1, score: 1, numberRoutes: 0 },
        { length: 2, score: 2, numberRoutes: 0 },
        { length: 3, score: 4, numberRoutes: 0 },
        { length: 4, score: 7, numberRoutes: 0 },
        { length: 6, score: 15, numberRoutes: 0 },
        { length: 8, score: 21, numberRoutes: 0 },
    ]);

    const handleRouteChange = (length, numberRoutes) => {
       const newRoutes = routes.map(elem => 
            if (elem.length === length){
                return {...elem, score: numberRoutes*length, numberRoutes}
            }
            return elem;
       )
       setRoutes(newRoutes);
    }

    const buttons = routes.map((route) => 
       <RouteScoreButton length={route.length} score={route.score} key={route.length} handleRouteChange={handleRouteChange}/>)

    return (
        <div className="position-absolute top-50 start-50 translate-middle">
            <div className="bg-light border border-primary rounded-3 px-4 py-4 opacity-75 text-center">
                <table className="table">
                    <thead>
                        <tr className="text-center">
                            <th scope="col">Route length</th>
                            <th scope="col">Total routes</th>
                            <th scope="col">Total score</th>
                        </tr>
                    </thead>
                    <tbody className="text-center align-middle">
                        {buttons}
                    </tbody>
                </table>
                <h3>Total score:</h3>
            </div>
        </div>
    )
}

and in child

const RouteScoreButton = (props) => {
  
    const increaseRoute = () => {
        props.handleRouteChange(props.length, props.numberRoutes + 1)
    }
    const decreaseRoute = () => {
        props.numberRoutes > 0 && props.handleRouteChange(props.length, props.numberRoutes - 1)
    }

    const handleChange = (e) => {
        let newValue = parseInt(e.target.value)
        if (isNaN(newValue)) {
            props.handleRouteChange(props.length, 0)
        } else {
            props.handleRouteChange(props.length, parseInt(e.target.value))
        }
    }

    return (
        <tr>
            <th>
                <h3>{props.length}</h3>
            </th>
            <th>
                <div className="input-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-primary" onClick={increaseRoute}>+</button>
                    <input type="text" className="form-control text-center" style={{ width: '50px' }} value={numberRoutes} onChange={handleChange} />
                    <button type="button" className="btn btn-primary" onClick={decreaseRoute}>-</button>
                </div>
            </th>
            <th>
                <h3>{score}</h3>
            </th>
        </tr>
    )
}

This will be a better approach.

For some reason if you still want parent to access the child state. you can check useImperativeHandle hook.