hexaquark
hexaquark

Reputation: 941

React how to set all remaining sibling's state to false whenever one sibling's state is true

Problem Description

I am trying to find a way to, whenever an arrow is set to rotate, reset to the initial position the remaining arrows.

Given :

const dropDownImage = 'https://i.postimg.cc/k57Q5cNL/down-arrow.png';

function App() {
    const information = ['a' ,'b', 'c'];
  
    return (
       <p>
         {information.map((value, index) => (
                <Child index={index} key={index}/>
         ))}
       </p>
    )
}
   
const Child = ({information, index, key}) => {
  const [dropDown, setDropDown] = React.useState(false);
  
  return (
      <img className={dropDown ? 'dropArrow doRotate' : 'dropArrow'}                        src={dropDownImage} width="50" width="50"
           onClick={
             ()=>{setDropDown(!dropDown)}
           }
      />
  )
}

ReactDOM.render(<App />, document.querySelector("#app"));
.dropArrow {
   position: relative;
    transition: .25s;
}

.doRotate {
    position:relative;
    transition: 1s;
    animation-fill-mode: forwards !important;
    animation: rotate 1s;
}

@keyframes rotate {
    100% {transform: rotate(180deg);}
}
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
  <div id="app"></div>

with JSFiddle Link Here

Restrictions

This is the structure of my larger project, so I can't change the modularization presented here.


What I have tried

I tried setting a hook in the parent component const [resetOthers, setResetOthers] = useState(null), pass the setter function as a prop to the child and trigger setResetOthers(index) from the child whenever setDropDown(!dropDown) is called to rotate the arrow. I then added the resetOthers as a key and made it such that an arrow animates only if it corresponds to the resetOthers index, but that didn't work. My arrows just multiplied on every click.

I tried migrating my setDropDown hook to the parent and handling the logic from there, but then I would need 3 different hooks and it would defeat the purpose of making a child in the first place.

Does anyone know how I can solve this problem ?

Upvotes: 1

Views: 269

Answers (3)

Ori Drori
Ori Drori

Reputation: 192857

Manage the state of the children in the parent by using useState() to hold the currently selected item. Whenever an item is clicked, assign it's value to the state, or remove it if it's already selected:

const dropDownImage = 'https://i.postimg.cc/k57Q5cNL/down-arrow.png';

const App = ({ items }) => {
  const [selected, setSelected] = React.useState(null);

  return (
    <p>
    {information.map(value => (
      <Child
        key={value} 
        onClick={() => setSelected(s => s === value ? null : value)} 
        selected={selected === value} />
    ))}
    </p>
  )
}
   
const Child = ({ selected, onClick }) => (
  <img className={selected ? 'dropArrow doRotate' : 'dropArrow'}                      src={dropDownImage}
        width="50"
        width="50"
    onClick={onClick}
  />
)

const information = ['a' ,'b', 'c'];

ReactDOM.render(<App items={information} />, document.querySelector("#app"));
.dropArrow {
  position: relative;
  transition: transform 1s;
}

.doRotate {
  position: relative;
  transform: rotate(180deg);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Upvotes: 1

Jacobo
Jacobo

Reputation: 1391

I modified your code, you need to manage the state in the parent component, and update it on each click. In order to be able to switch the state from the child component, you can pass a method from the parent component into the children as a prop. In this case I use that prop alongside the index to properly update the state. In addition, I also passed the value (true or false), as a prop to orient the chevron correctly.

import React, { Component } from "react";
import "./styles.css";
const dropDownImage = "https://i.postimg.cc/k57Q5cNL/down-arrow.png";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      information: [false, false, false]
    };

    this.updateInformation = this.updateInformation.bind(this);
  }

  updateInformation(index) {
    let newInformation = this.state.information;
    for (let i = 0; i < newInformation.length; ++i)
      newInformation[i] = i == index ? true : false;

    this.setState({ information: newInformation });
  }

  render() {
    return (
      <p>
        {this.state.information.map((value, index) => (
          <Child
            index={index}
            key={index}
            value={value}
            updateInformation={this.updateInformation}
          />
        ))}
      </p>
    );
  }
}

class Child extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <img
        className={this.props.value ? "dropArrow doRotate" : "dropArrow"}
        src={dropDownImage}
        width="50"
        width="50"
        onClick={() => {
          this.props.updateInformation(this.props.index);
        }}
      />
    );
  }
}

I attached you a link to the code sandbox so you can play with it: https://codesandbox.io/s/heuristic-wozniak-q5gq7?file=/src/App.js

Upvotes: 1

jered
jered

Reputation: 11581

A classic case of needing to "lift up state". In React, state should live at the lowest common denominator above which all components will need access to it. In other words, if multiple components will need to read (or write!) to a given piece of state, it should live one level (or more, but as few as possible) above those components that will need access.

In your case you should be tracking your "dropdown" state not in each child component, but in the App component. Then, the App component can pass that state to each child and provide a mechanism to modify it.

First let's refactor so that dropdown active state is kept in App instead of each child:

const dropDownImage = 'https://i.postimg.cc/k57Q5cNL/down-arrow.png';

function App() {
    const information = ['a' ,'b', 'c'];
    // Use an array, with each value representing one "dropdown" active state
    const [dropDowns, setDropDowns] = React.useState([false, false, false]);

    return (
       <p>
         {information.map((value, index) => (
                <Child
                 key={index}
                 index={index}
                 // isActive is passed in as a prop to control active state of each child
                 isActive={dropDowns[index]}
                 toggle={
                  () => {
                    // the toggle function gets provided to each child,
                    // and gives a way to update dropDowns state
                    setDropDowns(oldDropDowns => {
                      const newDropDowns = [...oldDropDowns];
                      newDropDowns[index] = !newDropDowns[index];
                      return newDropDowns;
                    });
                  }
                } />
         ))}
       </p>
    )
}

// now <Child> is completely stateless, its active state is controlled by App
const Child = ({information, index, toggle, isActive}) => {
  return (
      <img className={isActive ? 'dropArrow doRotate' : 'dropArrow'}                        src={dropDownImage} width="50" width="50"
           onClick={toggle}
      />
  )
}

From there it is trivial to modify the toggle callback to update state differently -- say, by resetting all dropdown isActive statuses to false except for the clicked one:

toggle={
    () => {
        setDropDowns(oldDropDowns => {
            const newDropDowns = oldDropDowns.map(ea => false);
            newDropDowns[index] = true;
            return newDropDowns;
        });
    }
}

Upvotes: 1

Related Questions