How to use the useState hook to count frequencies of items in an array(React useState hook)?

Viewed 1145

I'm trying to solve this exercise where the array contains 10 objects such as

arr = [{txt: "txt1", color: "red"},

{txt: "txt2", color: "green"}, {txt: "txt3", color: "blue"},...]

When I click a button, it should show up a random color which I know how to do but I'm not sure how to also show how many times each color shows up.

I set up my function as below:

const countColors = (props) => {
   

const arr = [{txt: "txt1", color: "red"},
{txt: "txt2", color: "green"}, {txt: "txt3", color: "blue"},...]

const [count, setCount] = useState(0);

const choice = () => {
    const randIdx = Math.floor(Math.random() * arr.length);
    return arr[randIdx];
};

const updateScore = () => {
        let randColor = choice(props.arr).color;
        if (randColor === 'red') {
            return setCount((count) => count + 1)
        } else if (randColor === 'green') {
            return setCount((count) => count + 1);
        } else {
            return setCount((count) => count + 1);
        }
    };
const clickHandler = () => {
setCount(updateScore);
}

return (
    <ul>
        <li>Red Counts: {updateScore}</li>
        <li>Green Counts: {updateScore}. 
        </li>
        <li>Blue Counts: {updateScore}</li>
    </ul> 
    <button onClick={clickHandler}>Click</button>

 )
};

I keep getting "NaN" or "undefined" when I check the state with the react dev tool.

I wonder if I need to set up 3 states for counting each color.

3 Answers

This is what you need to do:

const[redCount,setRedCount]=useState(0);
//similarily for other colors
const clickHandler = () => {
let randColor = choice(props.arr).color;
if (randColor === 'red') {
            setRedCount(redCount+1);
    //similairly do it for other colors

}

return (
    <ul>
        <li>Red Counts: {redCount}</li>
        //similarily for other colors
    </ul> 
    <button onClick={clickHandler}>Click</button>

 )
import React, { useState } from "react";

const CountColors = (props) => {
    const arr = [
        { txt: "txt1", color: "red" },
        { txt: "txt2", color: "green" },
        { txt: "txt3", color: "blue" },
    ];

    const colors = {};
    arr.forEach((item) => {
        colors[item.color] = 0;
    });
    const [count, setCount] = useState(colors);

    const choice = () => {
        const randIdx = Math.floor(Math.random() * arr.length);
        return arr[randIdx];
    };

    const clickHandler = () => {
        let randColor = choice(props.arr).color;
        console.log(randColor);
        setCount((prevCount) => ({
            ...prevCount, // to presist the old data in the state (spread operator)
            [randColor]: prevCount[randColor] + 1, 
        }));
    };

    return (
        <>
            <ul>
                {arr.map((item) => (
                    <li>{item.color} count: {count[item.color]}</li>
                ))}
            </ul>
            <button onClick={clickHandler}>Click</button>
        </>
    );
};

export default CountColors;

Try this code it should work for the scenario you asked for.

There are two ways to achieve this problem

  1. By creating a separate state for each and every color in an array.
  2. By creating an array and storing their current value by mapping their index with the arr.
  3. Dynamically creating an object with the color name as key and count as value(recommended).

Because the accessing time (read and write) of the value is very fast while using an object compared with an array in this scenario though we don't know the index at the first point.

What I have done here is that

  1. At first I have created an empty object named colors for looped through the arr array and set the key as color name and set the default value 0.

  2. On each click event depending upon the choice I have incremented the value and the reason I used spread operator is that the value in useState is mutable.

  3. Finally in the return statement I have looped through the array and printed the item color name as well as the value which makes the code look good.

I have two things that might help you in the future

  1. The class/function name should start with a capital letter.
  2. Always returned HTML components should be wrapped with empty tags or React.Fragment.

Just to double check if I understand this correctly: You have an array of colours and text. You want to click on a button which will select a random color and tell you how many times said color appears. Assuming that is correct, one of the things that stands out from your code is the following blocks:

const [count, setCount] = useState(0);
....

const updateScore = () => {
        let randColor = choice(props.arr).color;
        if (randColor === 'red') {
            return setCount((count) => count + 1)
        } else if (randColor === 'green') {
            return setCount((count) => count + 1);
        } else {
            return setCount((count) => count + 1);
        }
    };

First of all, notice how you always use the same state, count. Assuming this was correctly used, you will always override that state, meaning every time you begin the count of a new color, it will start again.

Secondly, the manner in which you are using the setCount. The state setter function does not expect a function as a parameter, but the actual value you want. To fix this initial proble, the proper set of the count should be done as:

setCount(count + 1)

However, this is where my first point comes to play: every time you try to set a new count, it will reset that of another color. So this is no good either.

Now, this leaves you with two choices: you can add a separate setXCount for each color as you suggested, (setRedCount, setBlueCount, etc), or you could through them all into an object so that you have it all in single state and don't require a new one be added every time there is a new color. Something like this:

    const [countPerColor, setCountPerColor] = React.useState({});
    ...
    const updateScore = () => {

     const randColor = choice(arr).color;

        if (countPerColor[randColor]){
          countPerColor[randColor] ++
        } else {
          countPerColor[randColor] = 1
        }

        setCountPerColor({...countPerColor})
     };

Obviously, your return statement will need to be updated accordingly:

...
        <li>Red Counts: {countPerColor.red || 0}</li>
        <li>Green Counts: {countPerColor.green || 0}. 
        </li>
        <li>Blue Counts: {countPerColor.blue || 0}</li>

And now to conclude, here is a functioning demo of the app.

const App = (props) => {
    const arr = [
        {txt: "txt1", color: "red"},
        {txt: "txt2", color: "blue"},
        {txt: "txt3", color: "blue"},
        {txt: "txt4", color: "red"},
        {txt: "txt5", color: "green"},
        {txt: "txt6", color: "red"},
        {txt: "txt7", color: "green"},
        {txt: "txt8", color: "green"},
        {txt: "txt9", color: "red"},
        {txt: "txt10", color: "blue"},
        ]
    
  const [countPerColor, setCountPerColor] = React.useState({});

  const choice = () => {
      const randIdx = Math.floor(Math.random() * arr.length);
      return arr[randIdx];
  };

  const updateScore = () => {

     let randColor = choice(arr).color;

    if (countPerColor[randColor]){
      countPerColor[randColor] ++
    } else {
      console.warn('exists', randColor)
      countPerColor[randColor] = 1
    }
    setCountPerColor({...countPerColor})
      };

  return (
  <div>
  <ul>
    <li>Red Counts: {countPerColor.red || 0}</li>
    <li>Green Counts: {countPerColor.green || 0}. 
    </li>
    <li>Blue Counts: {countPerColor.blue || 0}</li>
</ul> 
  <button onClick={() => updateScore()}>Click me</button>
  </div>
  )
}


ReactDOM.render(
    <App />,
    document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Related