How to assign refs to multiple components - javascript

I'm using React to render multiple data using array.map.
How can disable the clicked button from the list?
This is my code:
onRunClick(act, e) {
this.refs.btn.setAttribute("disabled", true);
}
render () {
return (
<div>
{
this.state.acts.map((act) => {
let boundActRunClick = this.onRunClick.bind(this, act);
return (
<p key={act._id}>
Name: {act.name}, URL(s): {act.urls}
<button ref='btn' onClick={boundActRunClick}>Run</button>
</p>
)
})
}
</div>
);
}
}
Using refs doesn't work ... I think that I can't add a state since there are multiple buttons.

You should use ref callback instead of ref and also yes you need multiple refs, an array should be good
According to the docs:
React supports a special attribute that you can attach to any
component. The ref attribute takes a callback function, and the
callback will be executed immediately after the component is mounted
or unmounted.
When the ref attribute is used on an HTML element, the ref callback
receives the underlying DOM element as its argument.
ref callbacks are invoked before componentDidMount or
componentDidUpdate lifecycle hooks.
Using the ref callback just to set a property on the class is a common
pattern for accessing DOM elements. The preferred way is to set the
property in the ref callback like in the above example. There is even
a shorter way to write it: ref={input => this.textInput = input}.
String refs are a legacy and and as per the docs:
Legacy API: String Refs
If you worked with React before, you might be familiar with an older
API where the ref attribute is a string, like "textInput", and the DOM
node is accessed as this.refs.textInput. We advise against it
because string refs have some issues, are considered legacy, and are
likely to be removed in one of the future releases. If you’re
currently using this.refs.textInput to access refs, we recommend
the callback pattern instead.
constructor() {
super();
this.btn = [];
}
onRunClick(act, index, e) {
this.btn[index].setAttribute("disabled", true);
}
render () {
return (
<div>
{
this.state.acts.map((act, index) => {
let boundActRunClick = this.onRunClick.bind(this, act, index);
return (
<p key={act._id}>
Name: {act.name}, URL(s): {act.urls}
<button ref={(el) => this.btn[index] = el} onClick={boundActRunClick}>Run</button>
</p>
)
})
}
</div>
);
}

Like #ShubhamKhatri's answer using ref is an option. You can also achieve desired behavior with state too.
Example (Single Disabled Button Option)
class App extends Component{
constructor() {
super();
this.state = {
disabled: ''
};
}
onRunClick(act, index, e) {
this.setState({ disabled: act._id });
}
render() {
return (
<div>
{
this.state.acts.map((act, index) => {
let boundActRunClick = this.onRunClick.bind(this, act, index);
return (
<p key={act._id}>
Name: {act.name}, URL(s): {act.urls}
<button disabled={this.state.disabled === act._id} onClick={boundActRunClick}>Run</button>
</p>
)
})
}
</div>
);
}
}
Example (Multiple Disabled Button Option)
class App extends Component{
constructor() {
super();
this.state = {
buttons: {}
};
}
onRunClick(act, index, e) {
this.setState((prevState) => {
const buttons = Object.assign({}, prevState.buttons, { [act._id]: !prevState.buttons[act._id] });
return { buttons };
});
}
render() {
return (
<div>
{
this.state.acts.map((act, index) => {
let boundActRunClick = this.onRunClick.bind(this, act, index);
return (
<p key={act._id}>
Name: {act.name}, URL(s): {act.urls}
<button disabled={this.state.buttons[act._id] || false} onClick={boundActRunClick}>Run</button>
</p>
)
})
}
</div>
);
}
}

For function components (React 16+), you can approach it like the following:
/*
* #param {Object|Function} forwardedRef callback ref function or ref object that `refToAssign` will be assigned to
* #param {Object} refToAssign React ref object
*/
export function assignForwardedRefs(forwardedRef, refToAssign) {
if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(refToAssign)
} else {
forwardedRef.current = refToAssign
}
}
}
function MyComponent({
forwardedRef
}) {
const innerRef = useRef()
function setRef(ref) {
assignForwardedRefs(forwardedRef, ref)
innerRef.current = ref
}
return <div ref={setRef}>Hello World!</div>
}
export default React.forwardRef((props, ref) => <MyComponent {...props} forwardedRef={ref} />)

You can use the npm module react-multi-ref (a tiny library by me) to do this.
import React from 'react';
import MultiRef from 'react-multi-ref';
class Foo extends React.Component {
_actRefs = new MultiRef();
onRunClick(act, e) {
this._actRefs.map.get(act._id).setAttribute("disabled", true);
}
render () {
return (
<div>
{
this.state.acts.map((act) => {
let boundActRunClick = this.onRunClick.bind(this, act);
return (
<p key={act._id}>
Name: {act.name}, URL(s): {act.urls}
<button ref={this._actRefs.ref(act._id)} onClick={boundActRunClick}>Run</button>
</p>
)
})
}
</div>
);
}
}
Though in this specific case where you just want to change an attribute on an element, instead of using a ref you should do it through state and props on the <button> through React as in the answer by #bennygenel. But if you need to do something else (call an imperative DOM method on the button, read the value of an uncontrolled input element, read the screen position of an element, etc) then you'll need to use a ref like this.

Related

Render unique divs for each hovered element

minimum reproducible example: https://codesandbox.io/s/react-hover-example-tu1eu?file=/index.js
I currently have a new element being rendered when either of 2 other elements are hovered over. But i would like to render different things based upon which element is hovered.
In the example below and in the codepen, there are 2 hoverable divs that are rendered; when they are hovered over, it changes the state and another div is rendered. I would like for the HoverMe2 div to render text "hello2". Currently, whether i hover hoverme1 or 2, they both just render the text "hello".
import React, { Component } from "react";
import { render } from "react-dom";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false
};
}
handleMouseHover() {
this.setState(this.toggleHoverState);
}
toggleHoverState(state) {
return {
isHovering: !state.isHovering
};
}
render() {
return (
<div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>hello</div>}
</div>
);
}
}
render(<HoverExample />, document.getElementById("root"));
You need to keep the state of item which you have hovered that's for sure
const { Component, useState, useEffect } = React;
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
values: ['hello', 'hello2'],
value: 'hello'
};
}
handleMouseHover({target: {dataset: {id}}}) {
this.setState(state => {
return {
...state,
isHovering: !state.isHovering,
value: state.values[id]
};
});
}
render() {
return (
<div>
<div
data-id="0"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
data-id="1"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.value}</div>}
</div>
);
}
}
ReactDOM.render(
<HoverExample />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
You can pass the context text as shown in example. This is working code:
import React, { Component } from "react";
import { render } from "react-dom";
// Drive this using some configuration. You can set based on your requirement.
export const HOVER_Hello1 = "Hello1";
export const HOVER_Hello2 = "Hello2";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
contextText: ""
};
}
handleMouseHover = (e, currentText) => {
this.setState({
isHovering: !this.state.isHovering,
contextText: currentText
});
}
toggleHoverState(state) {
//
}
render() {
return (
<div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello1)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello1)}
>
Hover Me
</div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello2)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello2)}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.contextText}</div>}
</div>
);
}
}
export default HoverExample;
If the whole point is about linking dynamically messages to JSX-element you're hovering, you may store that binding (e.g. within an object).
Upon rendering, you simply pass some anchor (e.g. id property of corresponding object) within a custom attribute (data-*), so that later on you may retrieve that, look up for the matching object, put linked message into state and render the message.
Following is a quick demo:
const { Component } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const data = [
{id:0, text: 'Hover me', message: 'Thanks for hovering'},
{id:1, text: 'Hover me too', message: 'Great job'}
]
class HoverableDivs extends Component {
state = {
messageToShow: null
}
enterHandler = ({target:{dataset:{id:recordId}}}) => {
const {message} = this.props.data.find(({id}) => id == recordId)
this.setState({messageToShow: message})
}
leaveHandler = () => this.setState({messageToShow: null})
render(){
return (
<div>
{
this.props.data.map(({text,id}) => (
<div
key={id}
data-id={id}
onMouseEnter={this.enterHandler}
onMouseLeave={this.leaveHandler}
>
{text}
</div>
))
}
{
this.state.messageToShow && <div>{this.state.messageToShow}</div>
}
</div>
)
}
}
render (
<HoverableDivs {...{data}} />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
As #CevaComic pointed out, you can do this with CSS. But if you want to use React, for example, because your actual problem is more complex, here is the answer.
You will need a way to tell apart the two elements. It could be done with some neat tricks, like setting an unique id to each element, passing a custom argument, or something else.
But I would advise against "cool tricks" as it's more difficult to understand what is going on, and the code is more prone to errors. I think the best way it to use a dumb approach of unique functions for unique elements.
Each onMouseEnter and onMouseLeave has to be an unique function (e.g. handleMouseHover1 and handleMouseHover2), and each of those functions need to control unique state (for example, isHovering1 and isHovering2). Then you have to render the element you want based on the state. Of course, for a real-world code, you will probably want to use more descriptive names to make the code more comprehensible. The full code would look something like this.
class HoverExample extends Component {
state = {
isHovering1: false,
isHovering2: false
};
handleMouseHover1 = () => {
this.setState(({ isHovering1 }) => ({ isHovering1: !isHovering1 }));
};
handleMouseHover2 = () => {
this.setState(({ isHovering2 }) => ({ isHovering2: !isHovering2 }));
};
render() {
const { isHovering1, isHovering2 } = this.state;
return (
<div>
<div
onMouseEnter={this.handleMouseHover1}
onMouseLeave={this.handleMouseHover1}
>
Hover Me1
</div>
<div
onMouseEnter={this.handleMouseHover2}
onMouseLeave={this.handleMouseHover2}
>
Hover Me2
</div>
{isHovering1 && <div>hello1</div>}
{isHovering2 && <div>hello2</div>}
</div>
);
}
}
Also, updated example: https://codesandbox.io/s/react-hover-example-rc3h0
Note: I have also edited the code to add some syntax sugar which exists with newer ECMAScript versions. Instead of binding the function, you can use the arrow function format, e.g. fn = () => { ... }. The arrow function means the this context is automatically bound to the function, so you don't have to do it manually. Also, you don't have to initialize this.state inside the constructor, you can define it as a class instance property. With those two things together, you do not need the constructor at all, and it makes the code a bit cleaner.

How to pass the props dynamically?

I am trying to learn how to use react-polls (for further communication with backend, so, some fragments of my code are prepared for next steps). The problem is: I expect to see 123 in my browser (as in componentDidMount), but I don't.
When I pass the string '123' directly instead of const pollQuestion = props => (<div>{props.quest}</div>); it works.
//imports
import Poll from 'react-polls';
const pollQuestion = props => (<div>{props.quest}</div>);
const pollAnswers = [
{ option: 'Yes', votes: 8 },
{ option: 'No', votes: 2 }
]
class PollQuestion extends Component {
state = {
pollAnswers: [...pollAnswers]
}
handleVote = voteAnswer => {
const { pollAnswers } = this.state
const newPollAnswers = pollAnswers.map(answer => {
if (answer.option === voteAnswer) answer.votes++
return answer
})
this.setState({
pollAnswers: newPollAnswers
})
}
render () {
const { pollAnswers } = this.state
return (
<div>
<Poll question={pollQuestion} answers={pollAnswers} onVote={this.handleVote} />
<p>It works</p>
</div>
);
}
};
class QuestionList extends Component {
state = {
questions: []
}
componentDidMount(){
this.setState({ questions: [{question_text:'123'}]});
}
render(){
return (
<div>{this.state.questions?
<ul>
<PollQuestion quest={this.state.questions.slice(0, 1).map(question => <li>{question.question_text}</li>)} />
</ul>:null}
</div>
)
}
};
function AppV() {
return (
<div className="App">
<QuestionList/>
</div>
);
}
export default AppV;
The problem was you were not passing the value of this.props.quest to the pollQuestion element. Either do this
<Poll question={this.props.quest} answers={pollAnswers} onVote={this.handleVote} />
See code here : https://stackblitz.com/edit/react-nngrut
OR do this
Edit
const pollQuestion = props => (<div>{props.quest}</div>); to
const MyPollQuestion = props => (<div>{props.quest}</div>);
<Poll question={<MyPollQuestion quest={this.props.quest} />} answers={pollAnswers} onVote={this.handleVote} />
https://stackblitz.com/edit/react-sxf2kx?file=AppV.js
Also, All react components should start with a capital letter.
The
React-Polls question attribute has Proptype of string
and you are
trying to pass a React Element
to it that is why it is not working.
Pass you question string directly.

Can't get store value after the value was updated using React

I have a SearchBar component and it has a subcomponent SearchBarItem.
I passed the method handleSelectItem() to subcomponent to dispatch value to store and it works (I saw it from the Redux tool in Chrome).
Then, when I tried to get the value from the method submitSearch(), which I also passed it from the parent component, it shows:
Cannot read property 'area' of undefined.
I'm still not so familiar with React. If someone can help, it will be very appreciated. Thanks in advance.
This is parent component SearchBar:
class SearchBar extends Component {
handleSelectItem = (selectCategory, selectedItem) => {
if (selectCategory === 'areas') {
this.props.searchActions.setSearchArea(selectedItem);
}
}
submitSearch() {
console.log(this.props.area); // this one is undefined
}
render() {
return (
<div className="searchBar">
<SearchBarItem
selectCategory="areas"
name="地區"
options={this.props.areaOptions}
handleSelectItem={this.handleSelectItem}
submitSearch={this.submitSearch}
/>
</div>
);
}
}
const mapStateToProps = state => ({
area: state.search.area,
brandOptions: state.search.brandOptions,
vehicleTypeOptions: state.search.vehicleTypeOptions,
areaOptions: state.search.areaOptions,
});
const mapDispatchToProps = dispatch => ({
searchActions: bindActionCreators(searchActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
This is subcomponent SearchBarItem:
export default class SearchBarItem extends Component {
state = {
showOptions: false,
selectedItem: [],
}
handleSelectItem = (selectedItem) => this.props.handleSelectItem(this.props.selectCategory, selectedItem);
submitSearch = () => this.props.submitSearch();
handleClickCategory = () => {
this.setState({ showOptions: !this.state.showOptions });
}
handleClickItem(option) {
this.setState({
selectedItem: [...this.state.selectedItem, option],
}, () => this.handleSelectItem(this.state.selectedItem));
}
render() {
const options = this.props.options.map((item, index) => (
<div
className={this.state.selectedItem === item ? "searchBarItem__option--active" : "searchBarItem__option"}
key={index}
onClick={() => this.handleClickItem(item)}
>
{item}
</div>
));
const optionBox = (
<div className="searchBarItem__box">
<div
className="searchBarItem__option"
onClick={() => this.handleClickItem('')}
>
不限{this.props.name}
</div>
{options}
<div className="searchBarItem__confirm">
<span>取消</span><span onClick={() => this.submitSearch()} >套用</span>
</div>
</div>
);
return (
<div className="searchBarItem">
<span onClick={() => this.handleClickCategory()} >
{(() => {
switch (this.state.selectedItem.length) {
case 0: return this.props.name;
case 1: return this.state.selectedItem[0];
default: return `${this.state.selectedItem.length} ${this.props.name}`;
}
})()}
</span>
{ this.state.selectedItem.length > 0 ? '' : <Icon icon={ICONS.DROP_DOWN} size={18} /> }
{ this.state.showOptions ? optionBox : '' }
</div>
);
}
}
SearchBarItem.propTypes = {
name: PropTypes.string.isRequired,
selectCategory: PropTypes.string.isRequired,
options: PropTypes.arrayOf(PropTypes.string).isRequired,
handleSelectItem: PropTypes.func.isRequired,
submitSearch: PropTypes.func.isRequired,
};
Your problem caused by the behavior of this pointer in javascript.
By writing the code submitSearch={this.submitSearch} you are actually sending a pointer to the submitSearch method but losing the this pointer.
The method actually defers as MyClass.prototype.myMethod. By sending a pointer to the method MyClass.prototype.myMethod you are not specifying to what instance of MyClass it belongs to (if at all). This is not the most accurate explanation of how this pointer works but it's intuitive explanation, you can read more here about how this pointer works
You have some possible options to solve it:
Option one (typescript/babel transpiler only) - define method as class variable
class MyClass{
myMethod = () => {
console.log(this instanceof MyClass) // true
}
}
this will automatically do option 2 for you
Option two - Bind the method on the constructor
class MyClass{
constructor(){
this.myMethod = this.myMethod.bind(this)
}
myMethod() {
console.log(this instanceof MyClass) // true
}
}
By the second way, you are binding the method to current this instance
Small note, you should avoid doing:
<MyComponent onSomeCallback={this.myCallback.bind(this)} />
Function.prototype.bind returns a new method and not mutating the existing one, so each render you'll create a new method and it has performance impact on render (binding it on the constructor only once as option two, is fine)

Passing a custom argument to the eventListener in React [duplicate]

We should avoid method binding inside render because during re-rendering it will create the new methods instead of using the old one, that will affect the performance.
So for the scenarios like this:
<input onChange = { this._handleChange.bind(this) } ...../>
We can bind _handleChange method either in constructor:
this._handleChange = this._handleChange.bind(this);
Or we can use property initializer syntax:
_handleChange = () => {....}
Now lets consider the case where we want to pass some extra parameter, lets say in a simple todo app, onclick of item i need to delete the item from array, for that i need to pass either the item index or the todo name in each onClick method:
todos.map(el => <div key={el} onClick={this._deleteTodo.bind(this, el)}> {el} </div>)
For now just assume that todo names are unique.
As per DOC:
The problem with this syntax is that a different callback is created
each time the component renders.
Question:
How to avoid this way of binding inside render method or what are the alternatives of this?
Kindly provide any reference or example, thanks.
First: A simple solution will be to create a component for the content inside a map function and pass the values as props and when you call the function from the child component you can pass the value to the function passed down as props.
Parent
deleteTodo = (val) => {
console.log(val)
}
todos.map(el =>
<MyComponent val={el} onClick={this.deleteTodo}/>
)
MyComponent
class MyComponent extends React.Component {
deleteTodo = () => {
this.props.onClick(this.props.val);
}
render() {
return <div onClick={this.deleteTodo}> {this.props.val} </div>
}
}
Sample snippet
class Parent extends React.Component {
_deleteTodo = (val) => {
console.log(val)
}
render() {
var todos = ['a', 'b', 'c'];
return (
<div>{todos.map(el =>
<MyComponent key={el} val={el} onClick={this._deleteTodo}/>
)}</div>
)
}
}
class MyComponent extends React.Component {
_deleteTodo = () => {
console.log('here'); this.props.onClick(this.props.val);
}
render() {
return <div onClick={this._deleteTodo}> {this.props.val} </div>
}
}
ReactDOM.render(<Parent/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
EDIT:
Second: The other approach to it would be to use memoize and return a function
constructor() {
super();
this._deleteTodoListener = _.memoize(
this._deleteTodo, (element) => {
return element.hashCode();
}
)
}
_deleteTodo = (element) => {
//delete handling here
}
and using it like
todos.map(el => <div key={el} onClick={this._deleteTodoListener(el)}> {el} </div>)
P.S. However this is not a best solution and will still result in
multiple functions being created but is still an improvement over the
initial case.
Third: However a more appropriate solution to this will be to add an attribute to the topmost div and get the value from event like
_deleteTodo = (e) => {
console.log(e.currentTarget.getAttribute('data-value'));
}
todos.map(el => <div key={el} data-value={el} onClick={this._deleteTodo}> {el} </div>)
However, in this case the attributes are converted to string using toString method and hence and object will be converted to [Object Object] and and array like ["1" , "2", "3"] as "1, 2, 3"
How to avoid this way of binding inside render method or what are the
alternatives of this?
If you care about re-rendering then shouldComponentUpdate and PureComponent are your friends and they will help you optimize rendering.
You have to extract "Child" component from the "Parent" and pass always the same props and implement shouldComponentUpdate or use PureComponent. What we want is a case when we remove a child, other children shouldn't be re-rendered.
Example
import React, { Component, PureComponent } from 'react';
import { render } from 'react-dom';
class Product extends PureComponent {
render() {
const { id, name, onDelete } = this.props;
console.log(`<Product id=${id} /> render()`);
return (
<li>
{id} - {name}
<button onClick={() => onDelete(id)}>Delete</button>
</li>
);
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
products: [
{ id: 1, name: 'Foo' },
{ id: 2, name: 'Bar' },
],
};
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete(productId) {
this.setState(prevState => ({
products: prevState.products.filter(product => product.id !== productId),
}));
}
render() {
console.log(`<App /> render()`);
return (
<div>
<h1>Products</h1>
<ul>
{
this.state.products.map(product => (
<Product
key={product.id}
onDelete={this.handleDelete}
{...product}
/>
))
}
</ul>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Demo: https://codesandbox.io/s/99nZGlyZ
Expected behaviour
<App /> render()
<Product id=1... render()
<Product id=2... render()
When we remove <Product id=2 ... only <App /> is re-rendered.
render()
To see those messages in demo, open the dev tools console.
The same technique is used and described in article: React is Slow, React is Fast: Optimizing React Apps in Practice by François Zaninotto.
Documentation encourages to use data-attributes and access them from within evt.target.dataset:
_deleteTodo = (evt) => {
const elementToDelete = evt.target.dataset.el;
this.setState(prevState => ({
todos: prevState.todos.filter(el => el !== elementToDelete)
}))
}
// and from render:
todos.map(
el => <div key={el} data-el={el} onClick={this._deleteTodo}> {el} </div>
)
Also note that this makes sense only when you have performance issues:
Is it OK to use arrow functions in render methods?
Generally speaking, yes, it is OK, and it is often the easiest way to
pass parameters to callback functions.
If you do have performance issues, by all means, optimize!
This answer https://stackoverflow.com/a/45053753/2808062 is definitely exhaustive, but I'd say fighting excessive re-renders instead of just re-creating the tiny callback would bring you more performance improvements. That's normally achieved by implementing a proper shouldComponentUpdate in the child component.
Even if the props are exactly the same, the following code will still re-render children unless they prevent it in their own shouldComponentUpdate (they might inherit it from PureComponent):
handleChildClick = itemId => {}
render() {
return this.props.array.map(itemData => <Child onClick={this.handleChildClick} data={itemData})
}
Proof: https://jsfiddle.net/69z2wepo/92281/.
So, in order to avoid re-renders, the child component has to implement shouldComponentUpdate anyway. Now, the only reasonable implementation is completely ignoring onClick regardless of whether it has changed:
shouldComponentUpdate(nextProps) {
return this.props.array !== nextProps.array;
}

How can I use 'this' in defaultProps React.Component class syntax?

In the past I was able to set a default prop that used this like so...
let MyButton = React.createClass({
getDefaultProps() {
return {
buttonRef: (ref) => this.button = ref
};
},
But now that I'm using JS classes MyButton looks like this...
class MyButton extends React.Component {
static defaultProps = {
buttonRef: (ref) => this.button = ref
};
And I get an error saying that this is undefined.
What do I need to do to be able to set default props that use this?
EDIT: Add some context
Setting a default buttonRef prop allowed me to use the ref in the MyButton component but also always be able to pass in a custom ref if a parent component needs to access the MyButton DOM node.
class MyButton extends React.Component {
static defaultProps = {
buttonRef: (ref) => this.button = ref
};
componentDidMount() {
Ladda.bind(this.button);
}
render() {
return (
<button
ref={(ref) => this.button = this.props.buttonRef(ref)}
onClick={this.props.handleClick}
>
{this.props.buttonText}
</button>
);
}
}
So then my button can always get hooked in to Ladda: Ladda.bind(this.button)
And if I need to access that button's DOM node in a parent component I can do so by passing in buttonRef as a prop like...
class MouseOverButton extends React.Component {
componentDidMount() {
this.mouseEnterButton.addEventListener("mouseover", doSomething(event));
}
render() {
return (
<div>
<MyButton
buttonRef={(ref) => this.mouseEnterButton = ref}
/>
</div>
);
}
}
EDIT: Apparently my simplified example doesn't illustrate the point well enough so I can come up with a more practical example or y'all can just answer the original question: What do I need to do to be able to use this in my defaultProps? Can I no longer do that using JS class syntax?
Where this convention of having a defaultProp for a specific element's ref has been useful is when using a HOC that hooks a nested component into some 3rd party API. I have an AddressSearch HOC that takes a node via a function passed to the wrapped component. Then it uses that node to hook it up with Google's Places API.
So I've got my addAddressSearch(component) function from my HOC. It adds the functions needed to hook up the Google places API. But for Google's API to work I need to know what DOM node I'm working with. So I pass my Input component an inputRef that gives my AddressSearchInput access to the appropriate node.
class AddressSearchInput extends React.Component {
static defaultProps = {
inputRef: (ref) => this.addressSearchInput = ref
};
componentDidMount() {
let node = this.addressSearchInput;
this.props.mountAddressSearch(node);
}
render() {
return (
<div>
<Input
inputAttributes={inputAttributes}
inputRef={(ref) => this.addressSearchInput = this.props.inputRef(ref)}
labelText={<span>Event address or venue name</span>}
labelClassName={labelClassName}
/>
</div>
);
}
}
AddressSearchInput = addAddressSearch(AddressSearchInput);
module.exports = AddressSearchInput;
// Here's the Input component if that helps complete the picture here
class Input extends React.Component {
render() {
return (
<div>
<Label>{this.props.labelText}</Label>
<HelperText text={this.props.inputHelperText} />
<input
{...this.props.inputAttributes}
ref={this.props.inputRef}
></input>
</div>
);
}
}
So now when I want to use my AddressSearchInput in a parent component that needs to add an eventListener to the relevant node I can just pass AddressSearchInput an inputRef prop.
class VenueStreetAddress extends React.Component {
componentDidMount() {
let node = this.venueStreetAddressInput;
this.props.mountValidateOnBlur(node, venueValidationsArray);
},
render() {
return (
<div>
<AddressSearchInput
inputRef={(ref) => this.venueStreetAddressInput = ref}
hasError={this.props.hasError}
/>
{this.props.errorMessageComponent}
</div>
);
}
}
And I can use AddressSearchInput all over the place and it doesn't break anything.
class UserStreetAddress extends React.Component {
componentDidMount() {
let node = this.userStreetAddressInput;
this.props.mountValidateOnBlur(node, userValidationsArray);
},
render() {
return (
<div>
<AddressSearchInput
inputRef={(ref) => this.userStreetAddressInput = ref}
hasError={this.props.hasError}
/>
{this.props.errorMessageComponent}
</div>
);
}
}
Maybe this way is convoluted and wrong but I don't have the time to figure out another way to do it on my own. So either point me to a tutorial(s) on the best way to hook into 3rd party APIs and add dynamic form validation without using refs or answer my original question which is...
What do I need to do to be able to use this in my defaultProps? Can I no longer do that using JS class syntax?
EDIT: In attempting to explain my use case I had the idea to make my defaultProps look like this...
static defaultProps = {
inputRef: (ref) => ref
};
which seems to be working without error.
In any case, the original question still stands. What do I need to do to be able to use this in my defaultProps? Can I no longer do that using JS class syntax?
This really should be a method, not a property.
class MyButton extends React.Component {
setButtonRef (ref) {
this.button = ref;
}
componentDidMount() {
Ladda.bind(this.button);
}
render() {
return (
<button
ref={ ref => this.setButtonRef(ref) }
onClick={this.props.handleClick}
>
{this.props.buttonText}
</button>
);
}
}
If you want a ref to the button, bind a variable at the class level and assign it to that class variable. Example:
export default class MyComponent extends Component {
componentDidMount() {
// button will be available as `this.button`
}
button = null; // initialize to null
render() {
return (
<div>
<Button ref={e => { this.button = e; }} />
</div>
);
}
}
Since a static property has no knowledge of class instances, if it's strictly necessary to make a static method aware of a given instance, the only way would be to pass to the static method the instance as an argument:
<button
ref={(ref) => this.button = this.props.buttonRef(this, ref)}
onClick={this.props.handleClick}
>
And in your defaultProp, use the instance:
static defaultProps = {
buttonRef: (instance, ref) => instance.button = ref
};

Categories

Resources