DEV Community

Cover image for Chaining State Update in ReactJS Efficiently
Mario Prasetya M.
Mario Prasetya M.

Posted on

Chaining State Update in ReactJS Efficiently

Managing states in ReactJS could be tricky. Changing state could trigger components or pages re-render. If it's not managed well, components re-render could lead to performance issues.

It is not uncommon to see a component that has dozens state. Some states are changed whenever other state changed. Also those state could make other states changed and so on. Fortunately, ReactJS has several utilities that could cover something like this. I will explain in a simple real use case how I approach this situation.

Use Case

Let's suppose there's form with 2 inputs as shown in the image below,

Use Case Form

By they way, I'm gonna implement the view by using React Typescript and Material UI. Typescript would be our best friend when building large scale application because of its type safety. Material UI would give us nice-looking theme while also giving us a lot of flexibility.

The first input is weight type that consists of two different options, Kg and Pound. Second input is number input for weight value. The second input has additional property that shows value unit in the end of the input. I'm gonna name this property adornment since Material UI also give the same name for this type of property.

The adornment value depends on first input. If user choose Kg for the value, weight value input adornment will show kg. Also for Pound value the adornment will show lb.

Our base view will be implemented like this,

import React from 'react'
import {
  Button,
  FormControl,
  FormControlLabel,
  FormLabel,
  InputAdornment,
  Radio,
  RadioGroup,
  TextField,
  Typography
} from '@mui/material'

function OrderForm() {
  return (
    <>
      <Typography variant="h2" fontWeight={600} fontSize={24} sx={{ mb: 2 }}>
        Order Form
      </Typography>
      <form
        style={{ width: '80%', maxWidth: '1000px', display: 'block', margin: 'auto' }}
      >
        <FormControl>
          <FormLabel id="payloadType-input" sx={{ textAlign: 'start' }}>
            Payload Type
          </FormLabel>
          <RadioGroup
            row
            aria-labelledby="payloadType-input"
            name="payloadType"
          >
            <FormControlLabel value="kg" control={<Radio />} label="Kg" />
            <FormControlLabel value="pound" control={<Radio />} label="Pound" />
          </RadioGroup>
          <FormLabel id="payloadWeight-input" sx={{ textAlign: 'start', mt: 1 }}>
            Weight
          </FormLabel>
          <TextField
            aria-labelledby="payloadWeight-input"
            name="payloadWeight"
            size='small'
            type="number"
            fullWidth
            InputProps={{
              endAdornment: <InputAdornment sx={{ width: 12 }} position="end">kg</InputAdornment>,
            }}
          />
          <Button type="submit" variant="contained" sx={{ mt: 3 }}>Submit</Button>
        </FormControl>
      </form>
    </>
  )
}

export default OrderForm

Enter fullscreen mode Exit fullscreen mode

Next let's implement the states.

Naive Approach

If we look into the problem at a glance, there are three states that we gonna maintain. The first and second state will be the input values. The third state will be the input adornment. It makes sense making it as a state since we need to re-render the screen whenever the adornment change. Our code will be something like this.

// ...

function OrderForm() {
  // States
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  const [adornmentLabel, setAdornmentLabel] = React.useState<string>('kg')

  return (
    // ...  
  )
}

export default OrderForm

Enter fullscreen mode Exit fullscreen mode

Our inputs will be controlled by payloadType and weight state. I will be using conventional input state binding for this example.


// ...

function OrderForm() {
  // States
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  const [adornmentLabel, setAdornmentLabel] = React.useState<string>('kg')

  React.useEffect(() => {
    // Assign new adornment
    const newAdornment = payloadType === 'pound' ? 'lb' : 'kg'
    setAdornmentLabel(newAdornment)
  }, [payloadType, setAdornmentLabel])

  return (
    <>
      <Typography variant="h2" fontWeight={600} fontSize={24} sx={{ mb: 2 }}>
        Order Form
      </Typography>
      <form
        style={{ width: '80%', maxWidth: '1000px', display: 'block', margin: 'auto' }}
      >
        <FormControl>
          <FormLabel id="payloadType-input" sx={{ textAlign: 'start' }}>
            Payload Type
          </FormLabel>
          <RadioGroup
            row
            aria-labelledby="payloadType-input"
            name="payloadType"
            // Input state binding
            onChange={(e) => setPayloadType(e.target.value)}
            value={payloadType}
          >
            <FormControlLabel value="kg" control={<Radio />} label="Kg" />
            <FormControlLabel value="pound" control={<Radio />} label="Pound" />
          </RadioGroup>
          <FormLabel id="payloadWeight-input" sx={{ textAlign: 'start', mt: 1 }}>
            Weight
          </FormLabel>
          <TextField
            aria-labelledby="payloadWeight-input"
            name="payloadWeight"
            size='small'
            type="number"
            fullWidth
            // Input state binding
            value={payloadWeight}
            onChange={(e) => setPayloadWeight(e.target.value)}
            // Replace with state
            InputProps={{
              endAdornment: <InputAdornment sx={{ width: 12 }} position="end">{adornmentLabel}</InputAdornment>,
            }}
          />
          <Button type="submit" variant="contained" sx={{ mt: 3 }}>Submit</Button>
        </FormControl>
      </form>
    </>
  )
}

// ...

Enter fullscreen mode Exit fullscreen mode

Whenever our payloadType value changes, our input adornment should be changed based on payloadType value. In ReactJS, in order to do something whenever some values change, typically we can use useEffect. The useEffect will receive payloadType state in its dependency array since we want to do something with input adornment whenever payloadType change.

Let's try our implementation.

Use Case Form after implementing the states

Yay, it works! But, does it?

The Problem

Surely it works perfectly. But let me show you something.

I'm gonna put a counter that's increased whenever the screen re-rendered. I will also log the counter in the console.


// ...

let counter = 0

function OrderForm() {
  // States
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  const [adornmentLabel, setAdornmentLabel] = React.useState<string>('kg')

  // Print and increase the value
  console.log('Screen is refreshed ', ++counter, ' times')

  // ...

Enter fullscreen mode Exit fullscreen mode

I will refresh the page once and change the payloadType value. Take a look at the logging result.

Re-render count in our Use Case Form

Initially, the component is mounted for the first time so the console shows 1 value. After that, I modify payloadType value. The console is showing 2 and followed by 3. Logically, the counter should only be increased 1 because I only change payloadType value once. What's happened?

Ah I see, the value is increased two times because there are two states change. The payload type input and then the input adornment.

Sure, that's the answer. But is it the best way we can do?

Doing sort kind of thing could lead into problem. For now it's fine because there are only two inputs and three states we maintain. But as our application grow, keeping this habit could lead into performance issue.

I did this once in my previous work. First there were only like five inputs and several states. Some of those states were related to input adornment and other supporting states that were updated whenever other states update.

As time went on, the requirements grew, and I still kept doing this habit. There were 15 inputs in total with their corresponding states and other states. There was notable input lag whenever user typed into field. Interacting with the form became very painful because of this.

There are some solutions for the problem, especially dealing with large form. But I will keep this topic for later. Right now I'm gonna focus on how do we chaining those states update so that we could minimize component re-render. So how do we tackle this problem?

Update States Synchronously

Forget about useEffect, forget about chaining states. Sometimes simplest way is the best way. Instead of update the adornment inside useEffect, why don't we update the adornment directly after updating the payloadType? In order to do this we have to move our payloadType onChange handler into separate function.


  // ...

  const payloadTypeChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPayloadType(e.target.value)
  }

  return (
    <>
      <Typography variant="h2" fontWeight={600} fontSize={24} sx={{ mb: 2 }}>
        Order Form
      </Typography>
      <form onSubmit={submitHandler} style={{ width: '80%', maxWidth: '1000px', display: 'block', margin: 'auto' }}>
        <FormControl>
          <FormLabel id="payloadType-input" sx={{ textAlign: 'start' }}>
            Payload Type
          </FormLabel>
          <RadioGroup
            row
            aria-labelledby="payloadType-input"
            name="payloadType"
            // Move this
            onChange={payloadTypeChangeHandler}
            value={payloadType}
          >

  // ...

Enter fullscreen mode Exit fullscreen mode

After that we can also update input adornment inside the same function.


// ...

  const payloadTypeChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPayloadType(e.target.value)

    const newAdornment = payloadType === 'pound' ? 'lb' : 'kg'
    setAdornmentLabel(newAdornment)
  }

// ...

Enter fullscreen mode Exit fullscreen mode

Finally we can remove our useEffect. Now our page would only by rendered once whenever our payloadType change.

Re-render count in Use Case Form after optimization

Using useMemo

I personally don't like the first approach. I think onChange function handler should only handle / control input change so that our handler won't be bloated. Whatever change that involve payloadType should stay away from our change handler.

We can achieve this by using useMemo. useMemo is a hook that save value as a result from complex calculation. Take a look at this example.

import React from 'react'
import {
  Button,
  FormControl,
  FormControlLabel,
  FormLabel,
  InputAdornment,
  Radio,
  RadioGroup,
  TextField,
  Typography
} from '@mui/material'

function OrderForm() {
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  // Take a look
  const adornmentLabel = payloadType === 'pound' ? 'lb' : 'kg'

  return (
    <>
      <Typography variant="h2" fontWeight={600} fontSize={24} sx={{ mb: 2 }}>
        Order Form
      </Typography>
      <form
        style={{ width: '80%', maxWidth: '1000px', display: 'block', margin: 'auto' }}
      >
        <FormControl>
          <FormLabel id="payloadType-input" sx={{ textAlign: 'start' }}>
            Payload Type
          </FormLabel>
          <RadioGroup
            row
            aria-labelledby="payloadType-input"
            name="payloadType"
            onChange={(e) => setPayloadType(e.target.value)}
            value={payloadType}
          >
            <FormControlLabel value="kg" control={<Radio />} label="Kg" />
            <FormControlLabel value="pound" control={<Radio />} label="Pound" />
          </RadioGroup>
          <FormLabel id="payloadWeight-input" sx={{ textAlign: 'start', mt: 1 }}>
            Weight
          </FormLabel>
          <TextField
            aria-labelledby="payloadWeight-input"
            name="payloadWeight"
            size='small'
            type="number"
            fullWidth
            value={payloadWeight}
            onChange={(e) => setPayloadWeight(e.target.value)}
            InputProps={{
              endAdornment: <InputAdornment sx={{ width: 12 }} position="end">{adornmentLabel}</InputAdornment>,
            }}
          />
          <Button type="submit" variant="contained" sx={{ mt: 3 }}>Submit</Button>
        </FormControl>
      </form>
    </>
  )
}

export default OrderForm

Enter fullscreen mode Exit fullscreen mode

I like this approach more. The above code could result the same behavior as before with one re-render. We have omitted the useState and use standard variable to save the value derived from payloadType. But this approach is not very good because this could lead into performance issue.

By processing a variable directly inside component, for every re-render, the process is executed. ReactJS components always be rendered many times, especially in large scale application. If the process you do is heavy and the component re-rendered many times, than we gonna have serious performance issue.

In order to limit processing variable, we can use useMemo hook. The hook can prevent the processing from happening by saving the result. useMemo also receives dependency array. The dependency array will make sure the process is executed only if array elements are changed.

We can implement it like this,


// ...

function OrderForm() {
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  // Take a look
  const adornmentLabel = React.useMemo<string>(
    () => payloadType === 'pound' ? 'lb' : 'kg',
    [payloadType]
  )

// ...

Enter fullscreen mode Exit fullscreen mode

useMemo receives two parameter. The first one is the function to process variable value and the second one is dependency array. By using useMemo, the adornmentLabel is changed only if payloadType is changed. We can put heavy process inside the function and it is only executed when elements inside dependency array are changed.


Closing

So that's all from me. What do you think of this approach? Let me know your thoughts :D

If you like my article, make sure to click the ❤️ button.

Top comments (0)