1

I am new to React and dev in general, but I am struggling to figure out how to achieve what I am trying to do. I feel as though I may have missed something along the way.

My goal is to have a list of items, that which on clicked individually, will toggle the visibility of their information.

The problem is that I am not able to map over the state in the parent element to display each object. But the state is in an array so I don't understand why it wouldn't be iterable. I do not get this problem when its just an object that I pass props to the child without state.

Is this the correct way to go about doing this? Am I supposed to create another array just to map over my object? I've also been a little confused as some sources create a class and use the constructor and render function. Is that deprecated or should I be doing it this way?

Parent

import React from "react";
import { useState } from "react";
//Components
import Card from "./Card";

const CardStack = () => {
  const [habits, setHabits] = [
    {
      id: 1,
      merit: "good",
      title: "Good Habit",
      count: 4,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 2,
      merit: "bad",
      title: "Bad Habit",
      count: 1,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 3,
      merit: "good",
      title: "Good Habit",
      count: 6,
      text: "Words to be hidden",
      visible: true,
    },
  ];

  const toggleCard = () => {
    this.setHabits((habit) => {
      habit.visible = !visible;
    });
  };

  return (
    <div className="card-stack">
      {habits.map((habit) => (
        <Card habit={habit} key={habit.id} onClick={toggleCard} />
      ))}
    </div>
  );
};

export default CardStack;

Child

import React from "react";

//Components
import Button from "./Button";

const Cards = ({ habit, onClick }) => {
  return (
    <div className="card" key={habit.id} onClick={onClick}>
      <h4 className="title" merit={habit.merit}>
        {habit.title}
        <div className="btn-group">
          <Button className="button" />
          <span className="count">{habit.count}</span>
          <Button className="button" />
        </div>
        {habit.visible ? (
          <div className="content">
            <p>visible</p>
          </div>
        ) : null}
      </h4>
    </div>
  );
};

export default Cards;
3
  • Very possible I'm just not familiar with this syntax, but did you forget useState? const [habits, setHabits] = useState([....]) Commented Sep 8, 2021 at 22:16
  • 1
    Your toggleCard method will overwrite habits, which starts as an array, with the boolean returned from habit.visible = !visible;. (And you don't call useState as noted by @talfreds). Also, this is not needed, and probably not pointing to the right 'this' anyway since you're using it in an arrow function. Commented Sep 8, 2021 at 22:17
  • Actually I'm almost sure the issue is not calling useState.. thus why habits is not iterable (since it's declared as the first object in the array rather than the array itself). Commented Sep 8, 2021 at 22:19

2 Answers 2

1

There are a number of problems with your code.

The first has been pointed out by @talfreds in their answer – you need to call useState() to initialize the state variable and its corresponding setter.

const CardStack = () => {
  const [habits, setHabits] = useState([
    {
      id: 1,
      merit: "good",
      title: "Good Habit",
      count: 4,
      text: "Words to be hidden",
      visible: false,
    },
    ...]);

Just doing this should allow your component to render.

But once you click the button, your current toggle handler will overwrite the array stored in habits with a boolean.

To fix this you need to understand that the callback you pass to setState is passed the current value of the relevant state variable for you to work with, and the state will be set to the value that you return from the callback. When working with arrays you need to avoid directly mutating this passed value, in this example by using map() which returns a new array, and by cloning the 'habit' object that we are changing use spread syntax.

const toggleCard = (id) => { // pass the id of the 'habit' to toggle
    setHabits((habits) => { // the current 'habits' array is passed to the callback
      // return a new array and avoid mutating nested objects when updating it
      return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
    });
  };


// usage
{habits.map((habit) => (
  ...
  <button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
  ...
)}

The last glaring problem is your use of this which is necessary when working with a class based component, but isn't necessary in a function component and actually won't work at all in the context of an arrow function.

Here is a shortened example snippet that may help you working through these ideas.

const { useEffect, useState } = React;

const App = () => {
  const [ habits, setHabits ] = useState([ // call useState to initialize 'habits' state
    {
      id: 1,
      merit: 'good',
      title: 'Good Habit',
      count: 4,
      text: 'Words to be hidden',
      visible: false,
    },
    {
      id: 2,
      merit: 'bad',
      title: 'Bad Habit',
      count: 1,
      text: 'Words to be hidden',
      visible: false,
    },
    {
      id: 3,
      merit: 'good',
      title: 'Good Habit',
      count: 6,
      text: 'Words to be hidden',
      visible: true,
    },
  ]);
  
  useEffect(() => {
    console.log('This: ', this);
  }, []);

  const toggleCard = (id) => { // id passed from mapped buttons
    setHabits((habits) => { // the current 'habits' array is passed to the callback
      // return a new array and avoid mutating nested objects when updating it
      return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
    });
  };

  return (
    <div className="card-stack">
      {habits.map((habit) => (
        <div key={habit.id} className="card">
          <h3>{habit.title}</h3>
          {habit.visible
            ? (<p>{habit.text}</p>)
            : null}
          <button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
        </div>
      ))}
    </div>
  );
};

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

<div id="root"></div>

Sign up to request clarification or add additional context in comments.

2 Comments

Wow thank you so much for pointing all this out to me! Forgetting useState was so obvious, that was silly of me...So, using spread would not be technically mutating the data because its cloning the data by referencing it? I also struggled in where I was supposed to call click handlers, and where to utilize the key prop, so this really helped a lot. Its been days trying to figure this out. But this really helped in strengthening my understanding, thanks for taking the time!
I'm glad it helped. Spreading an object in this way returns a new object and leaves the original object unchanged, so yes, this is a means of avoiding mutation. Keys should be placed at the top level of a mapped structure in general (and I was glad to see you had used a prop of the mapped elements as a key instead of index) though there are some use cases for placement of keys elsewhere. Also, please accept the answer if it helped you :)
0

Looks like you forgot to call useState?

    {
      id: 1,
      merit: "good",
      title: "Good Habit",
      count: 4,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 2,
      merit: "bad",
      title: "Bad Habit",
      count: 1,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 3,
      merit: "good",
      title: "Good Habit",
      count: 6,
      text: "Words to be hidden",
      visible: true,
    },
  ]);

As you have it:

    {
      id: 1,
      merit: "good",
      title: "Good Habit",
      count: 4,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 2,
      merit: "bad",
      title: "Bad Habit",
      count: 1,
      text: "Words to be hidden",
      visible: false,
    },
    {
      id: 3,
      merit: "good",
      title: "Good Habit",
      count: 6,
      text: "Words to be hidden",
      visible: true,
    },
  ];

habits.map()
-->Uncaught TypeError: habits.map is not a function```

2 Comments

Tip of the iceberg ;)
Very true but it explains the error and will allow them to start troubleshooting

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.