0

I'm new to React and I'm stuck trying to get this onClick function to work properly.

I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:

export function Row({parentState, setParentState}) {
    let divList = getDivList(parentState, setParentState);
    
    return (
        <div>
            {divList}
        </div>
    )
}

Say parentState could just be:

[["Name", "info"],
["Name2", "info2"]]

The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:

export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays

    const divList = parentState.map((ele, i) => {
        let divClass = "class" + ele[1];

        return (
            <div
                key={ele, i}
                className={divClass}
                onClick={() => {
                    let newParentState = 
                        JSON.parse(JSON.stringify(parentState);
                    newParentState[i][1] = "newInfo";

                    setParentState(newParentState);}}>
                {ele[0]}
            </div>
        )
    }
    return divList;
}

I have tried to use useEffect, probably wrong, but no luck. How should I do this?

  • What is this `parentState` that you are trying to render and update? – Drew Reese Feb 04 '22 at 00:34
  • It just contains information that's used in several components, including Row, throughout the parent component. It's an array of two-element arrays. –  Feb 04 '22 at 00:37
  • If it's an array of arrays, you should create a deep clone of the state, for example ```let newParentState = JSON.parse(JSON.stringify(parentState)```. Just reassigning it will create a reference which can cause bugs. Also, where is ```newValue``` coming from? – Chris B. Feb 04 '22 at 01:01
  • 1
    You should provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). Short of that, this is definitely a bug (you are mutating state): `let newParentState = parentState;`. Instead, you should create a new array for every array that is mutated: `let newParentState = parentState.map(c => [...c]);` – jsejcksn Feb 04 '22 at 01:03
  • Telling us it's just an array of two elements doesn't give us any more information that we couldn't already get from reading the code and seeing `ele[1]`.... but this is part of a value setting the `className` prop that you are specifically asking about. IMO that makes it a rather important detail. ***What*** its value is and ***how*** you are updating it. I do see that you are mutating your state with `newParentState[i][1] = newValue;`. What is `newValue` here? This seems to be what you are updating the `ele[1]` value to be for the `className` prop. – Drew Reese Feb 04 '22 at 01:20
  • Yes, `newValue` is the part that needs to be changed in the className of the divs. Because the code does update the `parentState` correctly, just later than I need it to, I didn't realize the method of getting `newValue` was important, since it is working properly. –  Feb 04 '22 at 01:35

1 Answers1

0

Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.

You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :

import React from 'react';
import { render } from 'react-dom';
import './style.css';

const App = () => {
  const [parentState, setParentState] = React.useState([
    ['I am a div', 'bg-red'],
    ['I am another div', 'bg-red'],
  ]);

  React.useEffect(
    () => console.log('render on ParentState changes'),
    [parentState]
  );

  const getDivList = () => {
    return parentState.map((ele, i) => {
      return (
        <div
          key={(ele, i)}
          className={ele[1]}
          onClick={() => {
            // Copy of your state with the spread operator (...)
            let newParentState = [...parentState];

            // We don't know the new value here, I just invented it for the example
            newParentState[i][1] = [newParentState[i][1], 'bg-blue'];

            setParentState(newParentState);
          }}
        >
          {ele[0]}
        </div>
      );
    });
  };

  return <Row>{getDivList()}</Row>;
};

const Row = ({ children }) => {
  return <>{children}</>;
};

render(<App />, document.getElementById('root'));

And a bit of css for the example :

.bg-red {
  background-color: darkred;
  color: white;
}

.bg-blue {
  background-color:aliceblue;
}

Here is a repro on StackBlitz so you can play with it.

I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.

Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Quentin Grisel
  • 4,794
  • 1
  • 10
  • 15
  • Thank you for your reply. I have changed it as you recommended. But, does this change the className onClick for you? This method of altering the className still doesn't work for me. It does eventually, just not right when you click it. I assume I have to trigger a re-render somehow, but I'm not sure how in this structure. –  Feb 04 '22 at 01:46
  • `newParentState[i] = [newParentState[i][0], 'bg-blue'];`.... need to shallow copy the `newParentState[i]` state being updated, otherwise it's still a state mutation. – Drew Reese Feb 04 '22 at 01:50
  • @unclear.chihuahua It does as you can see in the StackBlitz. The re-render is made when you edit the state `parentState` with `setParentState(newParentState);`. You can add a UseEffect to test if your re-render woths well : `React.useEffect(() => console.log("re-render on ParentState change"),[parentState]);` – Quentin Grisel Feb 04 '22 at 02:13
  • @DrewReese Missed this one thanks. While it works, it still is better to avoid it. – Quentin Grisel Feb 04 '22 at 02:19