Marwane Elalama
Marwane Elalama

Reputation: 73

Updates don't get displayed in the UI immediately in React

I'm trying to build a mini project in react, it is a BMI tracker, the user enters his weight and height, then the program calculates the BMI and whenever the application receives data (height and weight) from the user it then represents the result and the date at which the user entered that data in a graph. For that, I made three components, the first is the InputContainer component inside of which the calculations are performed.

import React, { useState, useRef, useEffect } from 'react';
import Graph from '../Graph/Graph';
import Logs from '../Logs/logs';
import './InputContainer.scss';
let dates = []
let bmiResults = []
let dataLog = [];
let canCalculate=0;
export default function InputContainer() {
    const [height, setHeight] = useState('');
    const [weight, setWeight] = useState('');
    const [log, setLog] = useState([]);
    const firstRender = useRef(true);
    useEffect(()=>{
        if (firstRender.current) {
            firstRender.current = false;
            return
        }
        canCalculate=0;
    },[height,weight])
    const calculate = () => {
        if(canCalculate){
            return
        }else{
            let bmi = (parseFloat(weight) / parseFloat(height) ** 2).toFixed(2)
            let date = new Date();
            let currentDate = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`
            let dataLogElement = <div className="test">
                        <div className="bmi-and-date">
                            <p className="bmi">
                                BMI: {bmi}
                            </p>
                            <p className="date">
                                Date: {currentDate}
                            </p>
                        </div>
                        <div className="height-and-weight">
                            <p className="height">
                                Height: {height}
                            </p>
                            <p className="weight">
                                weight: {weight}
                            </p>
                        </div>
                </div>;
            dates.push(currentDate);
            bmiResults.push(bmi);
            dataLog.push(dataLogElement);
            setLog(dataLog)
            canCalculate=1;
        }
    }

    return (
        <>
            <div className='input-container'>
                <h1>BMI Tracker</h1>
                <div className="input">
                    <div className="height">
                        <h3>Enter Your Height in M</h3>
                        <input type="number" onChange={(e) => {
                            setHeight(e.target.value)
                        }} value={height} />
                    </div>
                    <div className="weight">
                        <h3>Enter Your Weight in Kg</h3>
                        <input type="number" onChange={(e) => {
                            setWeight(e.target.value)
                        }} />
                    </div>
                </div>
                <button className='calculate' onClick={calculate} disabled={isNaN(parseFloat(height) && parseFloat(weight))}>Calculate</button>
            </div>
            <Graph dates={dates} results={bmiResults}/>
            <Logs element = {log}/>
        </>
    )
}

the second component is the Graph component which receives the user's data from props and then displays it in a graph:

import React, { useState, useRef, useEffect } from 'react';
import { Line } from 'react-chartjs-2';
import './Graph.scss';

export default function Graph(props) {
    const [data, setData] = useState({
        labels: props.dates,
        datasets: [
            {
                label: 'BMI',
                data: props.results,
                fill: 'origin',
                backgroundColor: '#16a5e1',
                pointBackgroundColor: '#16a5e1',
                pointBorderColor: 'blue',
                pointRadius: '4'
            },
        ],
    });
    const [options, setOptions] = useState({
        tension: 0.4,
        scales: {
            y: {
                ticks: {
                    color: 'white'
                },
                beginAtZero: true
            },
            x: {
                ticks: {
                    color: 'white'
                },
            }

        }
    });
    const firstRender = useRef(true);
    useEffect(()=>{
            if (firstRender.current) {
                firstRender.current = false;
                return
            }
            console.log(props.dates)
            console.log(props.results)
            setData({
                labels: props.dates,
                datasets: [
                    {
                        label: 'BMI',
                        data: props.results,
                        fill: 'origin',
                        backgroundColor: '#16a5e1',
                        pointBackgroundColor: '#16a5e1',
                        pointBorderColor: 'blue',
                        pointRadius: '4'
                    },
                ],
            });
            setOptions({
                tension: 0.4,
                scales: {
                    y: {
                        ticks: {
                            color: 'white'
                        },
                        beginAtZero: true
                    },
                    x: {
                        ticks: {
                            color: 'white'
                        },
                    }
                }
            });
        },[props]
    )
    return (
        <div className='graph-container'>
            <Line data={data} options={options} />
        </div>
    )
}

Then the third component which is the Logs component, displays the details of each user submission at the bottom of the page

import React, { useEffect, useState } from 'react'
import './logs.scss'
export default function Logs(props) {
    const [log,setLog] = useState('');
    useEffect(()=>{
        setLog(props.element)
    },[props])
    return (
        <div className='logs-container'>
            {log}
        </div>
    )
}

Once I start the app, I enter the weight and height then click the calculate button, the data gets plotted into the graph successfully and its corresponding details are logged at the bottom of the page, and this is exactly what I wanted, but the problem is that the data entered by the user gets displayed successfully only for the first entry, when I enter data a second time, calculations are done properly in the background and the results of the second entry are calculated, but nothing changes on the screen, and I can only see the results of the first entry, then once I start entering data for the third entry, I get the results of the first along with the second entry that I previously entered displayed on the screen, results only get displayed when I start entering data for the next entry and not immediately after clicking the calculate button.

And here is a link to the repo if you want to test it in your machine: https://github.com/Marouane328/Bmi-tracker

Upvotes: 0

Views: 40

Answers (1)

Daniel Baldi
Daniel Baldi

Reputation: 920

This is happening because you're directly mutating your state. It is a bit hard to see with this code, but the problem is in these two lines:

dataLog.push(dataLogElement);
setLog(dataLog);

See, what is happening here is that the first time that you run this calculate function, the log state becomes the dataLog array that you've declared. The problem is that every time that this functions runs again you will be pushing data directly into dataLog that is now your state (thus you will be mutating your state) and then you will be setting it to what it already is (which is dataLog!). This prevents React from realizing that things have changed. A bit confusing, but if you did not understand I'd suggest you to read about Mutating vs Non-Mutating methods and most importantly about variable assignment in JavaScript.

Changing the second line to setLog([...dataLog]) should be enough to fix this behavior. This way, every time that this code runs you'll be updating your state with a copy of the dataLog array instead of dataLog itself.

Upvotes: 2

Related Questions