React Hooks: Is there a way to calculate state values based on another state value

Viewed 2043

I'm looking for a way to calculate state values which depend on other state values using React Hooks.

I'm familiarizing myself with React Hooks. So far I've been using useState and useEffect. I'm aware of other hooks e.g. useReduce, useCallback, etc.

I've made a simple rates calculator that converts rates on hourly, weekly, monthly, and yearly basis. When I change the respective rate other rates will be updated accordingly.

The code for the app as follows:

import React, { useState } from 'react';
import {
    CssBaseline,
    Container,
    Typography,
    TextField
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(theme => ({
    paper: {
        marginTop: theme.spacing(8), // = 4 * 2
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center'
    }
}));

function App() {
    const classes = useStyles();
    const [hourlyRate, setHourlyRate] = useState(10);
    const [workHour, setWorkHour] = useState(40);
    const [weeklyRate, setWeeklyRate] = useState(400);
    const [monthlyRate, setMonthlyRate] = useState(1600);
    const [yearlyRate, setYearlyRate] = useState(19200);

    const tryConvert = ({ name, value }) => {
        if (name === 'work-hours') {
            setWorkHour(value);
            setWeeklyRate(value * hourlyRate);
            setMonthlyRate(value * hourlyRate * 4);
            setYearlyRate(value * hourlyRate * 4 * 12);
        } else if (name === 'hourly-rate') {
            setHourlyRate(value);
            setWeeklyRate(value * workHour);
            setMonthlyRate(value * workHour * 4);
            setYearlyRate(value * workHour * 4 * 12);
        } else if (name === 'weekly-rate') {
            setWeeklyRate(value);
            setHourlyRate(value / workHour);
            setMonthlyRate(value * 4);
            setYearlyRate(value * 4 * 12);
        } else if (name === 'monthly-rate') {
            setMonthlyRate(value);
            setWeeklyRate(value / 4);
            setHourlyRate(value / workHour / 4);
            setYearlyRate(value * 12);
        } else if (name === 'yearly-rate') {
            setYearlyRate(value);
            setMonthlyRate(value / 12);
            setWeeklyRate(value / 12 / 4);
            setHourlyRate(value / 12 / 4 / workHour);
        }
    };

    return (
        <Container component="main" maxWidth="xs">
            <CssBaseline />
            <div className={classes.paper}>
                <Typography component="h1" variant="h5">
                    Rate Kalkulator
                </Typography>
                <TextField
                    variant="outlined"
                    margin="normal"
                    id="hourly-rate"
                    label="Hourly Rate"
                    name="hourly-rate"
                    autoFocus
                    inputProps={{ 'data-testid': 'hourly-rate' }}
                    value={hourlyRate ? hourlyRate : 0}
                    onChange={event => tryConvert(event.target)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    id="work-hours"
                    label="Work Hours"
                    name="work-hours"
                    inputProps={{ 'data-testid': 'work-hours' }}
                    value={workHour}
                    onChange={event => tryConvert(event.target)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    id="weekly-rate"
                    label="Weekly Rate"
                    name="weekly-rate"
                    inputProps={{ 'data-testid': 'weekly-rate' }}
                    value={weeklyRate ? weeklyRate : 0}
                    onChange={event => tryConvert(event.target)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    id="monthly-rate"
                    label="Monthly Rate"
                    name="monthly-rate"
                    inputProps={{ 'data-testid': 'monthly-rate' }}
                    value={monthlyRate ? monthlyRate : 0}
                    onChange={event => tryConvert(event.target)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    id="yearly-rate"
                    label="Yearly Rate"
                    name="yearly-rate"
                    inputProps={{ 'data-testid': 'yearly-rate' }}
                    value={yearlyRate ? yearlyRate : 0}
                    onChange={event => tryConvert(event.target)}
                />
            </div>
        </Container>
    );
}

export default App;

Codesandbox Link - tryConvert

Rate Kalkulator - tryConvert

The app worked as expected, as far as I've tested, haven't worked on the edge cases yet.

Simplifying Rate Conversion

I'm trying to simplify the conversion tryConvert with other React Hooks but haven't been successful. This is the code for the conversion from the codes above :


    const tryConvert = ({ name, value }) => {
        if (name === 'work-hours') {
            setWorkHour(value);
            setWeeklyRate(value * hourlyRate);
            setMonthlyRate(value * hourlyRate * 4);
            setYearlyRate(value * hourlyRate * 4 * 12);
        } else if (name === 'hourly-rate') {
            setHourlyRate(value);
            setWeeklyRate(value * workHour);
            setMonthlyRate(value * workHour * 4);
            setYearlyRate(value * workHour * 4 * 12);
        } else if (name === 'weekly-rate') {
            setWeeklyRate(value);
            setHourlyRate(value / workHour);
            setMonthlyRate(value * 4);
            setYearlyRate(value * 4 * 12);
        } else if (name === 'monthly-rate') {
            setMonthlyRate(value);
            setWeeklyRate(value / 4);
            setHourlyRate(value / workHour / 4);
            setYearlyRate(value * 12);
        } else if (name === 'yearly-rate') {
            setYearlyRate(value);
            setMonthlyRate(value / 12);
            setWeeklyRate(value / 12 / 4);
            setHourlyRate(value / 12 / 4 / workHour);
        }
    };

My attempt on using useEffect

What I've tried is using useEffect to add callbacks when the states are changed. The codes below illustrate what I've tried:

    import React, { useState, useEffect } from 'react';


    const [hourlyRate, setHourlyRate] = useState(10);
    const [workHour, setWorkHour] = useState(40);
    const [weeklyRate, setWeeklyRate] = useState(400);
    const [monthlyRate, setMonthlyRate] = useState(1600);
    const [yearlyRate, setYearlyRate] = useState(19200);

    useEffect(() => {
        setWeeklyRate(hourlyRate * workHour);
        setMonthlyRate(hourlyRate * workHour * 4);
        setYearlyRate(hourlyRate * workHour * 4 * 12);
    }, [hourlyRate, workHour]);

    useEffect(() => {
        setHourlyRate(weeklyRate / workHour);
        setMonthlyRate(weeklyRate * 4);
        setYearlyRate(weeklyRate * 4 * 12);
    }, [weeklyRate, workHour]);

    useEffect(() => {
        setHourlyRate(monthlyRate / 4 / workHour);
        setWeeklyRate(monthlyRate / 4);
        setYearlyRate(monthlyRate * 12);
    }, [monthlyRate, workHour]);

    useEffect(() => {
        setHourlyRate(yearlyRate / 12 / 4 / workHour);
        setWeeklyRate(yearlyRate / 12 / 4);
        setMonthlyRate(yearlyRate / 12);
    }, [yearlyRate, workHour]);

If I use this approach, when I change workHour the conversion would not work as expected. I think it is because the workHour has become dependency on the 4 different useEffect. I'm still not sure about this.

Codesandbox Link - useEffect

Rate Kalkulator - useEffect

if you change the work hour the conversion won't work


Currently, I'm trying other approaches using useReduce, but I'm still not sure if this would be the best way to convert the rates.

What would be the best way to simplify my tryConvert?

I would appreciate any help on this.

1 Answers

There's really no need for useEffect here. useEffect is for side effects (e.g. setInterval, document.title, things that aren't part of the render cycle).

Your first solution works fine, but you can simplify it by having a single source of truth for the rate, and calculating the other rates based on that one. Something like this:

function App() {
  const classes = useStyles();
  const [hourlyRate, setHourlyRate] = useState(10);
  const [workHour, setWorkHour] = useState(40);

  const convertTo = name => {
    const map = {
      weekly: hourlyRate * workHour,
      monthly: hourlyRate * workHour * 4,
      yearly: hourlyRate * workHour * 52
    };
    return map[name] || 0;
  };

  const convertFrom = name => e => {
    const { value } = e.target;
    const map = {
      weekly: value / workHour,
      monthly: value / workHour / 4,
      yearly: value / workHour / 52
    };
    setHourlyRate(map[name]);
  };

  return (
      <div className={classes.paper}>
        <Typography component="h1" variant="h5">
          Rate Kalkulator
        </Typography>
        <TextField
          label="Hourly Rate"
          value={hourlyRate || 0}
          onChange={e => setHourlyRate(e.target.value)}
        />
        <TextField
          label="Work Hours"
          value={workHour}
          onChange={e => setWorkHour(e.target.value)}
        />
        <TextField
          label="Weekly Rate"
          value={convertTo("weekly") || 0}
          onChange={convertFrom("weekly")}
        />
        <TextField
          label="Monthly Rate"
          value={convertTo("monthly") || 0}
          onChange={convertFrom("monthly")}
        />
        <TextField
          label="Yearly Rate"
          value={convertTo("yearly") || 0}
          onChange={convertFrom("yearly")}
        />
      </div>
  );
}

For a working example, here's a codesandbox: https://codesandbox.io/s/rate-kalkulator-ej5m8

Related