Home > Back-end >  TypeScript React - onChange event on <select> element only sets the previous value and not cur
TypeScript React - onChange event on <select> element only sets the previous value and not cur

Time:11-29

I'm working on a scheduling web application.

I am trying to implement a feature that detects the total hours between two times, startTime and endTime, which are selected on a form and stored via the useState hook:

    const [startTime, setStartTime] = useState("")
    const [endTime, setEndTime] = useState("")

    const [totalHours, setTotalHours] = useState(0)

The end goal is to calculate and print the total hours between both times next to "totalHours:" in the UI: Image of UI for selecting start, end times and total hours

My Issue: The onChange event is only updating startTime and endTime to the PREVIOUS state whenever I update their respective fields on the form.

For example, both start at "12:00AM". If I change the startTime to "1:00AM", when I read startTime using console.log(startTime), it prints "NaN". If I then change startTime a second time, say to "2:00AM", console.log(startTime) prints "1:00AM"

I tried googling this and only found other threads referring the state as a component prop, nothing that uses the useState hook:

react useState hook variable value don't update on input onChange event

React setState not Updating Immediately

My understanding is that the setter functions for the useState hook, e.g. setStartTime() and setEndTime(), run asynchronously and cause this error.

I would appreciate any help.

my React/Typescript code:

The form HTML:

    return(
            <form>

                ...

                    <label>
                        startTime:
                        {/* <input type="text" className={inputStyle} onChange={(e) => setStartTime(e.target.value)}/> */}
                        <div id="selectStartTime">
                            <select className={inputStyle} name="startTimeHour" id="startTimeHour" 
                            onChange={(e) => handleStartTimeChange(e.target.value)}> {/*set the time AND calculate total hours*/}
                                <option value="12:00AM">12:00AM</option>
                                <option value="1:00AM">1:00AM</option>
                                <option value="2:00AM">2:00AM</option>
                                <option value="3:00AM">3:00AM</option>
                                <option value="4:00AM">4:00AM</option>
                                <option value="5:00AM">5:00AM</option>
                                <option value="6:00AM">6:00AM</option>
                                <option value="7:00AM">7:00AM</option>
                                <option value="8:00AM">8:00AM</option>
                                <option value="9:00AM">9:00AM</option>
                                <option value="10:00AM">10:00AM</option>
                                <option value="11:00AM">11:00AM</option>
                                <option value="12:00PM">12:00PM</option>
                                <option value="1:00PM">1:00PM</option>
                                <option value="2:00PM">2:00PM</option>
                                <option value="3:00PM">3:00PM</option>
                                <option value="4:00PM">4:00PM</option>
                                <option value="5:00PM">5:00PM</option>
                                <option value="6:00PM">6:00PM</option>
                                <option value="7:00PM">7:00PM</option>
                                <option value="8:00PM">8:00PM</option>
                                <option value="9:00PM">9:00PM</option>
                                <option value="10:00PM">10:00PM</option>
                                <option value="11:00PM">11:00PM</option>
                            </select>
                        </div>
                    </label>

                    <br/>
                    <label>
                        endTime:
                        {/* <input type="text" className={inputStyle} onChange={(e) => setEndTime(e.target.value)}/> */}
                        <div id="selectEndTime">
                            <select className={inputStyle} name="endTimeHour" id="endTimeHour" 
                            onChange={(e) => handleEndTimeChange(e.target.value)}> {/*set the time AND calculate total hours*/}
                                <option value="12:00AM">12:00AM</option>
                                <option value="1:00AM">1:00AM</option>
                                <option value="2:00AM">2:00AM</option>
                                <option value="3:00AM">3:00AM</option>
                                <option value="4:00AM">4:00AM</option>
                                <option value="5:00AM">5:00AM</option>
                                <option value="6:00AM">6:00AM</option>
                                <option value="7:00AM">7:00AM</option>
                                <option value="8:00AM">8:00AM</option>
                                <option value="9:00AM">9:00AM</option>
                                <option value="10:00AM">10:00AM</option>
                                <option value="11:00AM">11:00AM</option>
                                <option value="12:00PM">12:00PM</option>
                                <option value="1:00PM">1:00PM</option>
                                <option value="2:00PM">2:00PM</option>
                                <option value="3:00PM">3:00PM</option>
                                <option value="4:00PM">4:00PM</option>
                                <option value="5:00PM">5:00PM</option>
                                <option value="6:00PM">6:00PM</option>
                                <option value="7:00PM">7:00PM</option>
                                <option value="8:00PM">8:00PM</option>
                                <option value="9:00PM">9:00PM</option>
                                <option value="10:00PM">10:00PM</option>
                                <option value="11:00PM">11:00PM</option>
                            </select>
                        </div>
                    </label>

                    
                    <br/>
                    <label>
                        totalHours: {}
                    </label>

                 ...

          </form>

The handler functions for onChange:

    const handleStartTimeChange = (time: string) => {
        setStartTime(time);
        calculateTotalHours();
    }

    const handleEndTimeChange = (time: string) => {
        setEndTime(time);
        calculateTotalHours();
    }

The function that calculates the total hours between startTime and endTime (This is where I console.log to see that the error is happening)

    // calculate total hours based on start and end time
    const calculateTotalHours = () => {

        // convert strings as times to ints with values from 0 to 23 to represent 24 hour time
        // where 0 = 12am and 23 = 11pm

        // NOTE: This is where I see my error occuring
        console.log(startTime, endTime)

        // Get hours value
        // All values before ":", split time by colon and get first value, convert to int 
        let startTimeValue = parseInt(startTime.split(":")[0]);
        let endTimeValue = parseInt(endTime.split(":")[0]);

        // if either time is 12, remove 12 hours
        if (startTimeValue === 12) {
            startTimeValue -= 12;
        }
        if (endTimeValue === 12) {
            endTimeValue -= 12;
        }

        // if either time has PM, add 12 hours respectively
        if (startTime.includes("PM")) {
            startTimeValue  = 12;
        }
        if (endTime.includes("PM")) {
            endTimeValue  = 12;
        }

        // calculate time between start and end times
        const total = endTimeValue - startTimeValue;

        // if that value is negative, return 0.
        if (totalHours < 0) {
            const total = 0;
            setTotalHours(total);
        }

        // else, return the value  
        setTotalHours(total);
    }

CodePudding user response:

My understanding is that the setter functions for the useState hook, e.g. setStartTime() and setEndTime(), run asynchronously and cause this error.

That's completely true.

You should write an useEffect on your startTime and endTime and then run your calculateTotalHours functions in it:

const handleStartTimeChange = (time: string) => {
  setStartTime(time);
}

const handleEndTimeChange = (time: string) => {
  setEndTime(time);
}

useEffect(() => {
  calculateTotalHours();
}, [startTime, endTime]);

CodePudding user response:

you should do your calculations on useEffect hook:

useEffect(() => {
    calculateTotalHours()
}, [startTime, endTime])

Call useEffect at the top level of your component to declare an Effect. See more on https://beta.reactjs.org/apis/react/useEffect#useeffect

CodePudding user response:

The thing that is happening in your code overall is that. You are not aware of the asynchronous behaviour of setState or setter functions. The updated value in the state variable will be available in the next iteration.

The function calculateTotalHours is called on the same level as the setState part. The state value from history will always be one back of the current level.

const handleStartTimeChange = (time: string) => {
    setStartTime(time);
    calculateTotalHours();
}

const handleEndTimeChange = (time: string) => {
    setEndTime(time);
    calculateTotalHours();
}

The time in args of both above functions will always carry the updated value at the current level. You can do either of the two things. You can pass time as an argument in these functions to the calculateTotalHours function or you can call calculateTotalHours inside useEffect or useLayoutEffect hook with dependency of startTime, endTime, like below. These hooks will trigger a callback whenever there is a change in the value of any of the dependencies.

useEffect(() => {
  calculateTotalHours();
}, [startTime, endTime]);

CodePudding user response:

While both the above answers work in so far as they use useEffect to recalculate totalHours when either startTime or endTime changes, you should recognize that totalHours is a derived state from the startTime and endTime components. You should not store derived state in a separate stateHook variable, it should be derived from existing state.

If you just move the calculateTotalHours function outside of your callbacks and put it in the body of your component, have it return the total hours, everything will work fine and you won't find yourself with totalHours state diverging from the other two state hooks.

    const calculateTotalHours = () => {
        let startTimeValue = parseInt(startTime.split(":")[0]);
        let endTimeValue = parseInt(endTime.split(":")[0]);

        // if either time is 12, remove 12 hours
        if (startTimeValue === 12) {
            startTimeValue -= 12;
        }
        if (endTimeValue === 12) {
            endTimeValue -= 12;
        }

        // if either time has PM, add 12 hours respectively
        if (startTime.includes("PM")) {
            startTimeValue  = 12;
        }
        if (endTime.includes("PM")) {
            endTimeValue  = 12;
        }

        // calculate time between start and end times
        const total = endTimeValue - startTimeValue;
        return Math.max(total, 0);
        
    }

and then render the result of that function

// In your component before return statement
const totalHours = calculateTotalHours():
// Wherever you want to print totalHours
<label>
totalHours: ${totalHours}
</label>
  • Related