Scoping issue react parent-child method ES6 [duplicate] - javascript

This question already has answers here:
Unable to access React instance (this) inside event handler [duplicate]
(19 answers)
Closed 7 years ago.
Im currently a little stuck with the following.
function Tag(props){
let {text, onRemove, key} = props;
return (
<span onClick={onRemove(key)} key={key}>{text}</span>
)
}
function TagInput (props) {
let {onChange, value, onKeyDown} = props;
return (
<input type="text" onChange={onChange} value={value} onKeyDown={onKeyDown} />
)
}
class TagsInput extends Component {
render() {
let { Tag, TagInput, state } = this.props;
console.log(state.tags);
return (
<div ref="div" onClick={(e) => this.handleClick(e)}>
{state.tags.map((tag) =>
<Tag
text={tag.text}
key={tag.id}
onRemove={this.handleRemove}
/>
)}
<TagInput
onChange={(e) => this.handleChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
/>
</div>
)
}
handleClick(e) {
console.log(e.target.value);
}
handleChange(e){
//console.log(e.target.value);
}
handleKeyDown(e){
//console.log('keycode', e.keyCode);
const { dispatch } = this.props;
if (e.keyCode === 32) {
dispatch(addTag(e.target.value));
}
if (e.keyCode === 8 && e.target.value.length === 0) {
dispatch(removeTag());
}
}
handleRemove(id){
const { dispatch } = this.props;
dispatch(removeTag(id));
}
}
TagsInput.propTypes = {
TagInput: React.PropTypes.func,
Tag: React.PropTypes.func,
removeKeyCodes: React.PropTypes.array,
addKeyCodes: React.PropTypes.array
};
TagsInput.defaultProps = {
TagInput: TagInput,
Tag: Tag,
removeKeyCodes: [8],
addKeyCodes: [9, 13]
};
I get the following error in console Uncaught TypeError: Cannot read property 'props' of undefined from method handleRemove line const { dispatch } = this.props. It seems like a scoping issue but I cannot seem to understand why this (no pun intended lol) is occurring.

ES6 classes do not automatically bind this to functions with the exception of the ones provided by extending Component such as componentDidMount etc..
from the docs
The ES6 way - bind this to your methods in your constructor, OR where you call them:
class TagsInput extends Component {
constructor (props) {
super(props)
this.handleRremove = this.handleRemove.bind(this)
}
OR
render() {
return (
<Tag
onRemove={this.handleRemove.bind(this)}
/>
}
The ES7 way #1: bind syntax
this.handleRemove = ::this.handleRemove
The ES7 way #2: class arrow functions (I think this is the best way):
handleRemove = (id) => {
const { dispatch } = this.props;
dispatch(removeTag(id));
}
Then call it like:
onRemove={ () => this.handleRemove(tag.id) }
UPDATE: Read #Road's answer as well. When binding the method at point of use you need to pass the argument along.

Have you tried binding this? Try this.handleRemove.bind(this, tag.id). The tag.id there is the argument you're passing since handleRemove(id) needs an id as an arg.

Related

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)

ref is not defined using arrow function in react?

I try to make a toggleable content when user clicked on outside of the element I got error of this.node is not defined error?
handleOutsideClick(e) {
// ignore clicks on the component itself
if (this.node.contains(e.target)) {
return;
}
this.handleClick();
}
render() {
return (
<div ref={node => { this.node = node; }}>
<button onClick={this.handleClick}>Button handler</button>
{this.state.visibleContent && <div>Toggle content</div>}
</div>
);
}
Code https://codesandbox.io/s/v38q4zrq7
In the render method I've used ref={node => { this.node = node; }} why is it still undefined? Here's a working example that used exactly the same technique https://codepen.io/graubnla/pen/EgdgZm
Your function handleOutsideClick is out of scope. If you're using babel, you can turn it into an arrow function directly
handleOutsideClick = (e) => {
// ignore clicks on the component itself
if (this.node.contains(e.target)) {
return;
}
this.handleClick();
}
or if that is not an option, bind it in your constructor
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}

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 to assign refs to multiple components

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.

How to access correct 'this' inside map: ReactJS [duplicate]

This question already has answers here:
How do I get the right "this" in an Array.map?
(5 answers)
Closed 3 years ago.
For example I have a react component with two binding methods:
import React from 'react';
class Comments extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemoveComment = this.handleRemoveComment.bind(this);
}
handleSubmit(e) {
.....
}
handleRemoveComment(e) {
//this.props.removeComment(null, this.props.params, i);
}
renderComment(comment, i) {
return(
<div className="comment" key={i}>
.....
<button
onClick={this.handleRemoveComment}
className="remove-comment">
×
</button>
</div>
)
}
render() {
return(
<div className="comments">
{this.props.postComments.map(this.renderComment)}
.....
</div>
)
}
}
export default Comments;
In above code, I have two binding method: one is handleSubmit and one is handleRemoveComment. handleSubmit function worked but handleRemoveComment doesn't. When running, It returns error:
TypeError: Cannot read property 'handleRemoveComment' of undefined
Issue is with this line:
{this.props.postComments.map( this.renderComment )}
Because you forgot to bind renderComment, map callback method, so this inside renderComment method will not refer to the class context.
Use any one of these solutions, it will work.
1- Use this line in constructor:
this.renderComment = this.renderComment.bind(this) ;
2- Pass this with with map like:
{this.props.postComments.map(this.renderComment, this)}
3- Use Arrow function with renderComment method, like this:
renderComment = (comment, i) => {
.....
or use the map inside the renderComment function (i used to prefer this way), like this:
renderComment() {
return this.props.postComments.map((comment, i) => {
return(
<div className="comment" key={i}>
<p>
<strong>{comment.user}</strong>
{comment.text}
<button
onClick={this.handleRemoveComment}
className="remove-comment">
×
</button>
</p>
</div>
)
})
}
And call this method from render, in this case binding of renderComment is not required:
{this.renderComment()}

Categories

Resources