How to target single item in list with onClick when mapping JSON array in React

Viewed 4455

I have a JSON file with an array of objects and each object consists of "Question" and "Answer" (I'm creating an FAQ section). What I'm doing is mapping over the array and displaying the list of questions, which works just fine. Next to each question is an icon and I want the icon to change when I click on it but it is changing EVERY icon in the list instead of just that one item that was clicked on.

I'm using Material UI and hooks and this is how I have my handleClick set up:

const [click, setClick] = useState(true);
const handleClick = () => {
   setClick(!click);
};

This is how I have the array mapping set up:

<List
   style={{
     maxHeight: 430,
     width: 500,
     overflow: 'auto',
     border: '1px solid black',
        }}
      >
        {faqdata.map((item) => (
          <ListItem style={{ cursor: 'pointer' }}>
            <ListItemIcon>
              {click ? <AddIcon /> : <RemoveIcon />}
            </ListItemIcon>
            <ListItemText primary={item.Question} onClick={handleClick} />
          </ListItem>
        ))}
      </List>

How can I make it to where the icon changes on only the list item that I click on instead of every list item in the list? Is my onClick in the incorrect spot? Any help would be greatly appreciated. Thanks!

3 Answers

Issue

You are using a single boolean value to store a "clicked" state, and all your mapped UI uses that single state to cue from.

Solution

Assuming you would like multiple items to be clicked, and also assuming your mapped data is static (i.e. the faqData isn't added to, removed from, or sorted) then using the mapped index to toggle the "clicked" state is acceptable. use an object to store "clicked" indices and update the handleClick callback to toggle the state. For this use case I like to make the callback a curried handler to enclose in scope the value I wish to use in the callback.

const [clickedIndex, setClickedIndex] = useState({});

const handleClick = (index) => () => {
  setClickedIndex(state => ({
    ...state, // <-- copy previous state
    [index]: !state[index] // <-- update value by index key
  }));
};

...

<List
  style={{
    maxHeight: 430,
    width: 500,
    overflow: 'auto',
    border: '1px solid black',
  }}
>
  {faqdata.map((item, index) => (
    <ListItem style={{ cursor: 'pointer' }}>
      <ListItemIcon>
        {clickedIndex[index] ? <AddIcon /> : <RemoveIcon />} // <-- check if index is truthy in clickedIndex state
      </ListItemIcon>
      <ListItemText
        primary={item.Question}
        onClick={handleClick(index)} // <-- pass index to handler
      />
    </ListItem>
  ))}
</List>

Is this what you're looking for?

import { useState } from "react";
import "./styles.css";

const faqdata = [
  { Question: "Q1", Answer: "A1" },
  { Question: "Q2", Answer: "A2" },
  { Question: "Q3", Answer: "A3" },
  { Question: "Q4", Answer: "A4" }
];

const AddIcon = () => <span class="icon">&#43;</span>;
const RemoveIcon = () => <span class="icon">&#9747;</span>;

function ListItem({ d }) {
  const [checked, setChecked] = useState(false);
  return (
    <li
      onClick={() => {
        setChecked(!checked);
      }}
    >
      {checked ? <RemoveIcon /> : <AddIcon />}
      {d.Question}
    </li>
  );
}

function List() {
  return (
    <ul>
      {faqdata.map((d) => {
        return <ListItem d={d} />;
      })}
    </ul>
  );
}

You can try it out here

The problem with the current approach is that there's only one variable to store the added/removed status of every question. So, when the click boolean updates, it updates the state of all elements.

In the code shared above, the ListItem component is responsible for maintaining the added/removed status of each question separately. So, one item in the list can change without affecting the other.

It's one of my test. You should save selected ids and check if the id exists in that array.

    const [selectedfaqdataIds, setSelectedfaqdataIds] = useState([]);
    const handleSelect = (event, id) => {
     const selectedIndex = selectedfaqdataIds.indexOf(id);
     let newselectedfaqdataIds = [];

     if (selectedIndex === -1) {
        newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds, id);
     } else if (selectedIndex === 0) {
        newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds.slice(1));
     } else if (selectedIndex === selectedfaqdataIds.length - 1) {
        newselectedfaqdataIds = newselectedfaqdataIds.concat(selectedfaqdataIds.slice(0, -1));
     } else if (selectedIndex > 0) {
        newselectedfaqdataIds = newselectedfaqdataIds.concat(
        selectedfaqdataIds.slice(0, selectedIndex),
        selectedfaqdataIds.slice(selectedIndex + 1)
      );
     }

     setSelectedfaqdataIds(newselectedfaqdataIds);
      };    


{faqdatas.map((faqdata) => (
       <ListItem style={{ cursor: 'pointer' }}>
            <ListItemIcon>
              {selectedfaqdataIds.indexOf(faqdata.id) !== -1}? <AddIcon /> : <RemoveIcon />}
            </ListItemIcon>
            <ListItemText primary={faqdata.Question} onClick={(event) => handleSelect(event, faqdata.id)} />
       </ListItem>
    ))}
Related