24

I'm mapping through an array and for each item display a button with text. Say I want that on clicking the button, the text underneath will change its color to red. How can I target the sibling of the button? I tried using ref but since it's a mapped jsx, only the last ref element will be declared.

Here is my code:

class Exams extends React.Component {
    constructor(props) {
        super()
        this.accordionContent = null;
    }
    state = {
        examsNames: null, // fetched from a server
    }
    accordionToggle = () => {
        this.accordionContent.style.color = 'red'
    }
    render() {
        return (
            <div className="container">
                {this.state.examsNames && this.state.examsNames.map((inst, key) => (
                    <React.Fragment key={key}>
                        <button onClick={this.accordionToggle} className="inst-link"></button>
                        <div ref={accordionContent => this.accordionContent = accordionContent} className="accordionContent">
                            <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aperiam, neque.</p>
                        </div>    
                    </React.Fragment>
                ))}
            </div>
        )
    }
}


export default Exams;

As explained, the outcome is that on each button click, the paragraph attached to the last button will be targeted.

Thanks in advance

sir-haver
  • 3,096
  • 7
  • 41
  • 85

4 Answers4

26

Initialize this.accordionContent as an array

constructor(props) {
    super()
    this.accordionContent =[];
}

And set the ref like this

<div ref={accordionContent => this.accordionContent[key] = accordionContent} className="accordionContent">
Prakash Sharma
  • 15,542
  • 6
  • 30
  • 37
2

Here is my working codepen example based on your code above

Linked example is an "actual" accordion, ie display and hide adjacent content.

(see code snippets below for turning to red)

https://codepen.io/PapaCodio/pen/XwxmvK?editors=0010


CODE SNIPPETS

initialize the referance array:

constructor(props) {
    super();
    this.accordionContent = [];
}

add the ref to the reference array using the key:

<div ref={ref => (this.accordionContent[key] = ref)} >

pass the key to the toggle function via onClick

 <button onClick={() => this.accordionToggle(key)} >

finally reference the key inside the toggle function

accordionToggle = key => {
    this.accordionContent[key].style.color = 'red'
};
PapaCodio
  • 21
  • 2
1

I found a way to do it without using ref, by using the key property of the map:

 accordionToggle = (key) => {
        console.log(key)
        var a = document.getElementsByClassName('accordionContent')
        a[key].style.color = 'red'
    }

I'm not sure if it's as good to access the dom like so, instead of using refs to directly target the elements.

sir-haver
  • 3,096
  • 7
  • 41
  • 85
1

In the below example I am using a refs array, initialized using useRef, for persistence across re-renders, and then fill the first time <Wrapper> is rendered, and from then on, all the refs creating within the map using React.createRef() will be cached within refs and ready for use whenever <Wrapper> re-renders.

Each dynamic ref (of the refs array) is being assigned as prop to to each child node:

const Wrapper = ({children}) => {
    const refs = React.useRef([]);

    // as the refs are assigned with `ref`, set each with a color "red"
    React.useEffect(() => {
      refs.current.map(ref => { 
        // no support here for optional chaining: ref?.current?.style.color
        if(ref.current) ref.current.style.color = 'red'
      })
    }, [refs]);

    // iterate the children and create a ref inide the "refs" array,
    // if one was not already added for this child's index.
    // use "cloneElement" to pass that ref to the children
    const withProps = React.Children.map(children, (child, i) => {
        // no support here for ?? instead of ||
        refs.current[i] = refs.current[i] || React.createRef();
        return React.cloneElement(child, {ref: refs.current[i] })
    });

    // no support for <> instead of useless `div` wrapper
    return <div>{withProps}</div>
};

ReactDOM.render(<Wrapper>
  <button>1</button>
  <button>2</button>
  <button>3</button>
</Wrapper>, document.body)
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
vsync
  • 118,978
  • 58
  • 307
  • 400