Skip the empty state in React.js

I’m trying to make an app where the value of the checkbox elements appear in an array, joined by ampersands.

For the most part, it’s working, except I have to click two boxes to get the list to start rendering.

https://usereducer.bl1xvan.repl.co/

How do I get the list to render when I click the first checkbox?

1 Like

It’s not that you have to click two, it’s that you’re rendering the tick after it happens. So you update once, but you’re not updating the state. The next time you click that actually updates the state to take into account the previous change, then you click again and it takes into account the previous change and so on.

The components are all functions, to get them to return an version of the rendering code relating to that updated list you need to run the relevant functions again, which isn’t happening here immediately.

There are several reasons why this might happen, all related to the fact you’re not properly updating the state and causing a rerender on click, but without any code it’s not very easy to provide help.

1 Like

My bad. here you go!

import React, {useState} from 'react'
import './App.css'

export default function App() {

  const [checkBox, setCheckBox] = useState([
    {id: 1, checked: false, name: "React.js"}, 
    {id: 2, checked: false, name: "JS"}, 
    {id: 3, checked: false, name: "CSS"}, 
    {id: 4, checked: false, name: "HTML"}])
  const [langValue, setLangValue] = useState("")

  const handleCheckBox = (id) => {
  setCheckBox((prev) => {
    return prev.map((item) => {
      if(item.id === id){
        return {...item, checked: !item.checked}
      }else{
        return {...item}
      }
    })
  })
  const filterCheckBox = checkBox.filter(item => item.checked === true)
  const filterMap = filterCheckBox.map(item => item.name);
  setLangValue(filterMap.join('&'))
}
  return (
    <div>
    {checkBox.map((item) => {
              return(
                <label><input type="checkbox" checked={item.checked} onChange={() => handleCheckBox(item.id)} />{item.name}</label>
              )
            })} 
    <p>{langValue}</p>
    </div>
  )
}
1 Like

These things happen at the same time, you:

  1. Update the array of checkboxes to a new value
  2. Set the text underneath to the current value

So those setStates do cause a rerender, but you’re explicitly setting the string of text to whatever the value was before the rerender. Just do the logic you want to create a new array of the checkboxes state + the derived display value, then pass those values to the setState calls.

What I said before applies absolutely here: those values in setState aren’t really that special, and the components are functions. When you setState it’s just taking the new values and passing them into the function again to rerun it. There’s no point between the setCheckBox and the next line in that handler function where the value of checkBox has changed, the whole function needs to run again before that happens. At which point checkBox is a new value, but then langValue is on the last value. And so on.

Fix is extremely simple, so for example (sorry, some of the names may be different as I wasn’t looking at the original code – tbh should try to use more descriptive names for clarity anyway but :person_shrugging: ):

export default function App() {
  const [checkboxes, setCheckboxes] = useState([
    {id: 1, checked: false, name: "React.js"}, 
    {id: 2, checked: false, name: "JS"}, 
    {id: 3, checked: false, name: "CSS"}, 
    {id: 4, checked: false, name: "HTML"},
  ]);
  const [langValue, setLangValue] = useState("");

  const updateDisplay = (id) => {
    const updatedCheckboxes = checkboxes.map((cbox) => {
      return cbox.id === id ? {...cbox, checked: !cbox.checked} : cbox;
    });
    const updatedLangValue = updatedCheckboxes.flatMap((cbox) => {
      return cbox.checked ? [cbox.name] : [];
    }).join(" & ");

    setCheckboxes(updatedCheckboxes);
    setLangValue(updatedLangValue);
  };

  return (
    <div>
    {checkboxes.map((item) => {
      return(
        <label><input type="checkbox" checked={item.checked} onChange={() => updateDisplay(item.id)} />{item.name}</label>
      )
    })} 
    <p>{langValue}</p>
    </div>
  )
}

Edit: it’s not a great idea to use nested structures as state. The issue is that you always have to be careful to ensure you make a new version every time; it makes it really easy to accidentally mutate instead of clone. And React does a check to see if it needs to update a value, so if there’s a mutation, the object reference hasn’t changed, so no update. Can’t really avoid this completely here, so I’d maybe do something like this:

function App({techsList}) {
  const [selectedTechs, setSelectedTechs] = useState([])

  const handleChange = (tech) => {
    if (selectedTechs.includes(tech) {
        setSelectedTechs(selectedTechs.concat(tech);
    } else {
setSelectedTechs(selectedTechs.filter((t) => t !== tech);
    }
  }

  return  (
    <div>
    {techsList.map((tech) => {
      return(
        <label>
          <input
            type="checkbox"
            checked={selectedTechs.includes(tech)}
            onChange={handleChange}
          />
  </label>
      )
    })} 
    <p>{selectedTechs.join(" & ")}</p>
    </div>
  )

Note above is completely untested and done on my phone, so I can’t vouch for it working as-is

I don’t see what the selectedPaths references. Did you mean to say selectedTechs?

Yeah just a typo, sorry

Edit: was written on my phone at stupid-o’clock in the morning when I was half asleep, working version here:

Edit: also working (do not copy this, it’s exists because I was trying to see how small I could make the state, how it works is not clear at all)

I think this could be another solution @Bl1xvan

import React, {useState, useEffect} from 'react'
import './App.css'

export default function App() {

  const [checkBox, setCheckBox] = useState([
    {id: 1, checked: false, name: "React.js"}, 
    {id: 2, checked: false, name: "JS"}, 
    {id: 3, checked: false, name: "CSS"}, 
    {id: 4, checked: false, name: "HTML"}])

  const [langValue, setLangValue] = useState("")

  const handleCheckBox = (id) => {
    setCheckBox((prev) => {
     return prev.map((item) => {
      if(item.id === id){
        return {...item, checked: !item.checked}
      }else{
        return {...item}
      }
    })
  })    
}
 useEffect(()=>{
  setLangValue(() => {
  const filterCheckBox = checkBox.filter(item => item.checked === true)
  const filterMap = filterCheckBox.map(item => item.name);
  return filterMap.join('&') })
}) 
  return (
    <div>
    {checkBox.map((item) => {
              return(
                <label><input type="checkbox" checked={item.checked} onChange={() => { handleCheckBox(item.id);  }} />{item.name}</label>
              )
            })} 
    <p>{langValue}</p>
    </div>
)}

Because useEffect will always be triggered after the state was set, and before the function returns (I think, but not a React expert by any means.)

The only change I did was to put the setLangValue in the useEffect hook

EDIT

The problem with the original code seems to be that the setLangValue does not wait the setCheckBox to finish, and then is always accessing an old value (the previous state of the array.)

It seems that react expects each state to be independent of each other. That is why useEffect is sort of useful (because it waits the first setCheckBox to be done.)

Many thanks for the tip!

1 Like