I'm developing a simple 'to do list' react app (new to React.js). I have adding items to a list working but deleting items raises a question. Within my parent react component, i have the following code:
import ToDoEntries from './to_do_entries.jsx';
class ToDoList extends React.Component {
constructor(props) {
super(props);
this.state = { list: [] }
this.add = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
}
addItem(e) { //removed to avoid tl:dr }
render() {
return(
<form onSubmit={this.add}>
<input placeholder='Enter item' type='text' ref={(el) => {this._input = el;} }/>
<button>Add</button>
</form>
<ToDoEntries entries={this.state.list}
removeCallback={this.removeItem}
/>
);
}
}
My to_do_entries.jsx component:
class ToDoEntries extends React.Component {
constructor(props) {
super(props);
}
renderItems() {
const { entries, removeCallback } = this.props;
function createTasks(item) {
return <li key={item.key}>{item.text}</li>
}
var listItems = entries.map(function(item) {
return(<li onClick={removeCallback} key={item.key}>{item.text}</li>)
})
return listItems;
}
render() {
var todoEntries = this.renderItems();
return(
<ul>
{todoEntries}
</ul>
);
}
}
export default ToDoEntries;
Running this code bring:
Warning: setState(…): Cannot update during an existing state
transition
Question:
why does to_do_entries.jsx's render immediately execute the callback when an item gets added i.e:
var listItems = entries.map(function(item) {
return(<li onClick={removeCallback(id)} key={item.key}>{item.text}</li>)
})
However, adding .bind(null, id) to removeCallback ie. <li onClick={removeCallback.bind(null, id)} /> does not?
Problem is in this part:
onClick={removeCallback(id)}
We need to pass a function to onClick, not the value. When we use () with functionName, that means you are calling that method and assigning the result of that to onClick, that will create a infinite loop if you do setState in removeCallback, because of this cycle:
render -> removeCallback() -> setState ->
^ |
| |
| |
-----------------------------------------
That's why you are getting the error.
Check the snippet for difference between abc and abc():
function abc(){
return 'Hello';
}
console.log('without () = ', abc); //will return the function
console.log('with () = ', abc()); //will return the function result (value)
Why it is working with onClick={removeCallback.bind(null, id)}?
Because bind will create a new function, and assign that function to click event, here removeCallback will get called when you click on any item not automatically.
As per MDN Doc:
The bind() function creates a new bound function (BF). A BF is an
exotic function object (a term from ECMAScript 2015) that wraps the
original function object. Calling a BF generally results in the
execution of its wrapped function.
Check the React DOC: Handling events in JSX.
Check this answer for more details about bind: Use of the JavaScript 'bind' method
I would advise against that, and use a similar approach to the example I have written for you. Render a list of todo's that is bind-ed to state and then pass in the relevant information back up to your parent component to remove the item. In this case, I use the index of the todo to splice the array so that it removes it.
Your current onClick is invoked immediately when each todo <li> is rendered because it's simply a function call which is causing the problem. .bind solves this problem because it will create a new function when you click on the element which is why the function doesn't invoke immediately.
However, this is generally considered bad practice because every time the component it'll create this function again and again and again. Multiple this by the amount of todo's on the screen and you'll be losing performance. It's a small issue, but my example shows how to solve this problem.
https://codepen.io/w7sang/pen/VWNLJp
// App
class App extends React.Component{
constructor(props) {
super(props);
this.state = { list: [] }
this.add = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
}
addItem(e) {
e.preventDefault();
this.setState({
list: [
...this.state.list,
{
key: Math.random(1,10000),
text: this._input.value
}
]
})
}
removeItem(payload){
this.setState({
list: [
...this.state.list.slice(0, payload.index),
...this.state.list.slice(payload.index + 1)
]
})
}
render() {
return(
<div>
<form onSubmit={this.add}>
<input placeholder='Enter item' type='text' ref={(el) => {this._input = el;} }/>
<button>Add</button>
</form>
<ToDoEntries entries={this.state.list} removeItem={this.removeItem} />
</div>
);
}
}
// TodoEntries [STATELESS]
const ToDoEntries = ({ entries, removeItem } ) => {
return(
<ul>
{ entries.map((item, index) => {
return(<Todo key={item.key} index={index} item={item} removeItem={removeItem} />)
}) }
</ul>
);
}
// Todo
class Todo extends React.Component {
constructor(props){
super(props);
this.state = {};
this.remove = this.remove.bind(this);
}
remove() {
const { index, removeItem } = this.props;
removeItem({
index
});
}
render() {
return <li onClick={this.remove}>{this.props.item.text}</li>
}
}
ReactDOM.render(<App />,document.getElementById('app'));
<div id="app"></div>
why does to_do_entries.jsx's render immediately execute the callback?
Well, when your mapping over the list of todo's each <li/> is invoking the removeCallback function instead of assigning it to the onClick.
So current code
<li onClick={removeCallback(id)} </li>
is equivalent to:
var result = removeCallback(id);
<li onClick={result} </li>
You have correctly pointed out that using bind will work. This is due to the behavior which makes it so useful in these situations.
See the mdn docs for more info, but I'll quote the important part here:
bind ... creates and returns a new function that, when called...
In your case, when using bind, and giving that to the onClick you are creating a new function that will be called when the click event is actually fired, not when the element is being rendered.
Another way of looking at removeCallback.bind(null, id) is its like this:
var newFunc = () => {
return removeCallback(id);
}
<li onClick={newFunc} </li>
Related
I am trying to display the data of each character when I click the Info Button.
I know it is because in the onButtonClickHandler function it can not see the state. I have also tried this.state.person but it gives me an error saying "can not read state". And if I try just state.person it will give me "undefined".
What is the best way to do that? Thank you
API Link: https://swapi.dev/people/
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ person: data.results, loading: false });
}
render() {
if (this.state.loading) {
return <div>loading...</div>;
}
if (!this.state.person.length) {
return <div>didn't get a person</div>;
}
function onButtonClickHandler(state) {
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}
Please correct me if I'm wrong
The most likely reason why you are seeing this is because of the way javascript internally works. The syntax:
function xyz() {
}
has an implicit this
Maybe try changing your code from:
function onButtonClickHandler(state) {
console.log(state.person);
};
to:
const onButtonClickHandler = () => {
console.log(this.state.person);
};
Further Reading: Here
You have defined your function onButtonClickHandler as a function that takes one argument, and logs the person property of that argument. The argument state in your function has nothing to do with the state of your component. As javascript sees it, they are two totally unrelated variables which just happen to have the same name.
function onButtonClickHandler(state) {
console.log(state.person);
};
When button calls onClick, it passes the event as the argument. So your onButtonClickHandler is logging the person property of the event, which obviously doesn't exist.
Since you are not using any information from the event, your function should take no arguments. As others have said, you should also move this function outside of the render() method so that it is not recreated on each render. The suggestion to use bind is not necessary if you use an arrow function, since these bind automatically.
export default class FetchActors extends React.Component {
/*...*/
onButtonClickHandler = () => {
console.log(this.state.person);
};
}
Inside render()
<button onClick={this.onButtonClickHandler}>Enter</button>
You could also define the function inline, as an arrow function which takes no arguments:
<button onClick={() => console.log(this.state.person)}>Enter</button>
If you are new to react, I recommend learning with function components rather than class components.
Edit:
Updating this answer regarding our comments. I was so caught up in explaining the errors from doing the wrong thing that I neglected to explain how to do the right thing!
I am trying to display the data of each character when I click the Info Button.
Once we call the API, we already have the info loaded for each character. We just need to know which one we want to display. You can add a property expanded to your state and use it to store the index (or id or name, whatever you want really) of the currently expanded item.
When we loop through to show the name and info button, we check if that character is the expanded one. If so, we show the character info.
Now the onClick handler of our button is responsible for setting state.expanded to the character that we clicked it from.
{this.state.person.map((person, i) =>(
<div>
<div>
{person.name}
<button onClick={() => this.setState({expanded: i})}>Info</button>
{this.state.expanded === i && (
<CharacterInfo
key={person.name}
person={person}
/>
)}
</div>
CodeSandbox Link
there are a few ways you can resolve your issue; I'll give you the more common approach.
You want to define your click handler as a class (instance) method, rather than declare it as a function inside the render method (you can define it as a function inside the render method, but that's probably not the best way to do it for a variety of reasons that are out of scope).
You will also have to bind it's 'this' value to the class (instance) because click handlers are triggered asynchronously.
Finally, add a button and trigger the fetch on click:
class Actors extends React.Component {
state = {
loading: false,
actors: undefined,
};
constructor(props) {
super(props);
this.fetchActors = this.fetchActors.bind(this);
}
async fetchActors() {
this.setState({ loading: true });
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
this.setState({ actors: data.results, loading: false });
}
render() {
console.log('Actors: ', this.state.actors);
return <button onClick={this.fetchActors}>fetch actors</button>;
}
}
Sometimes i takes react a min to load the updated state.
import React from "react";
export default class FetchActors extends React.Component {
state = {
loading: true,
person: null
};
async componentDidMount() {
const url = "https://swapi.dev/api/people/";
const response = await fetch(url);
const data = await response.json();
if(!data.results) { // throw error }
this.setState({ person: data.results, loading: false }, () => {
console.log(this.state.person) // log out your data to verify
});
}
render() {
if (this.state.loading || !this.state.person) { // wait for person data
return <div>loading...</div>;
}else{
function onButtonClickHandler(state) { // just make a componentDidUpdate function
console.log(state.person);
};
return (
<div>
<h1>Actors</h1>
{this.state.person.map(person =>(
<div>
<div>
{person.name}
<button onClick={onButtonClickHandler}>Info</button>
</div>
</div>
))}
<button onClick={onButtonClickHandler}>Enter</button>
</div>
);
}
}}
I am calling a handle method (to change state) in a <grandchild> component but it stop rendering after a couple of callback in the <grandparent> component.
I have tried to:
setting bind correctly with both this.bind in construct and arrow method.
making sure the call back is call everytime the prop.callback is call.
This is an example of what I'm trying to do with graphql server:
Grandparent Component
//Graphql constant for query
const ApolloConstant = gpl`
...etc
class Grandparent extends Component {
constructor(props) {
super(props);
this.state = { vars: 'query_string' }
}
handler = (args) => {
this.setState({vars: args})
}
render() {
return (
// For requerying graphql with search
<input onChange={() => this.setState(vars: e.target.value)} />
<Query variable={this.state.vars}>
...code -> some_data_arr
{<ChildComponent data={some_data_arr} handler={this.handler}/>}
</Query>
);
}
}
Child Component
//This component will take in an arr of obj and display a obj list
// if one of the obj is clicked then render a child component to display that single obj
class Child extends Component {
constructor(props) {
super(props);
this.state = {
singleData: null
}
}
render() {
return (
// Ternary operator here for conditional rendering
{
this.state.singleData
? <Grandchild data={this.state.singleData} handleParentData={this.props.handler} />
: this.display_data(this.props.data)
}
);
}
//Method to call to display objects
display_data = () => {
this.props.map() =>
<div onClick={this.setState({singleData: data})} > ...some code to display data <div/>
}
}
Grandchild Component
class Grandchild extends Component {
render() {
return (
{...do some code with object props here}
<Button onclick={(this.props.handleParentData(vars))} >Btn</Button>
);
}
}
When I test this, everything works for the first 3-4 render then no more re-rendering even though the callback is going through. I check to see if the method in <grandparent> is being call and it does but the state stop changing. Even after going to a new route (react router) and then coming back, I still cant change state with that callback.
<Button onclick={(this.props.handleParentData(vars))} >Btn</Button>
I think the problem is the function being called right into the onclick prop, you should probably have it wrapped in another function so it is only called when you actually trigger the onclick listener:
handleClick = () => {
this.props.handleParentData(vars)
}
render() {
return (
{...do some code with object props here}
<Button onclick={(this.handleClick)} >Btn</Button>
);
}
I'm trying to build a simple stock ticker app. Type in a symbol like AAPL, TSLA, GOOG and show the current price of all 3.
The problem is, after the first stock entered, I can't get the component to auto-update. It only updates on an interval (15 seconds via setInterval).
How do I get my component to update on form submit? In StockList I have props that contain the symbol names passed down from my main App component. I pass them down to StockInfo via props again, where I run getStock() for my API call to get prices. This works fine the first passthrough, but anytime I enter more stocks the component does not update unless setInterval runs. I'm running this.setState in StockInfo, but it doesn't seem to matter.
What am I missing?
class StockList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<StockInfo stocks={this.props.symbols} />
</div>
);
}
}
//
class StockInfo extends React.Component {
constructor(props) {
super(props);
this.state = {
symbols: [],
text: '',
stock: [],
time: new Date().toLocaleString()
};
}
static getDerivedStateFromProps(props, state) {
return {
symbols: props.stocks,
};
}
componentDidMount() {
this.intervalID = setInterval(
//interval
() => {this.getStocks()},
10 * 1000
);
this.getStocks();
console.log("mounting StockInfo")
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
tick() {
this.setState({
time: new Date().toLocaleString()
});
}
getStocks() {
this.tick();
var symbol_str = "";
var stock_list = [];
if (this.state.symbols.length > 0) {
for (let i = 0; i < this.state.symbols.length; i++) {
stock_list.push(this.state.symbols[i].text);
}
}
this.setState({symbols: stock_list});
symbol_str = stock_list.join();
//stock api call here
}
render() {
return (
<div>
<b>The time is {this.state.time}.</b>
<ul>
{
this.state.stock.map((obj, index) =>
// Only do this if symbols have no stable IDs
<li key={index}>
<span className="stock_name">{obj.quote.symbol}</span> - <span className="stock_latest_price">{obj.quote.latestPrice}</span>
</li>
)
}
</ul>
</div>
);
}
}
For updating state on every component update, you can use the getDerivedStateFromProps lifecycle (if react version > 16), else the componentDidUpdate lifecycle can be used.
Refer the link here
assuming you use the latest version of React, you may need to observe the changes of the props.
Check the documentation of the getDerivedStateFromProps method
https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
previously used componentWillReceiveProps(), but it was considered insecure and will be marked as deprecated in upcoming updates
I hope this helps you
You need to setState the stock on the form input as well:
<form>
<input onChange={(e) => this.setState({stock: e.target.value})}/>
</form>
Once you setState on input change it will re-render/update your component.
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;
}
Essentially, I want to invoke a callback passed down from a parent component then reassign a value. I've tried creating a class method that invokes the given callback function from the parent components props, but I'm getting an infinite loop with setState. On another method that I tried but cannot seem to replicate at the moment, an error was thrown that stated "callback is not a function".
Perhaps, I'm phrasing this in a weird way. Here's an example:
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = { parentState: true }
this._handleToggle = this._handleToggle.bind(this);
}
_handleToggle() {
this.setState({ parentState: !this.state.parentState })
}
render() {
return (
<ChildComponent
onSomeEvent={this._handleToggle}
/>
)
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.randomInteger = 8;
this._invokeCallback = this._invokeCallback.bind(this);
}
// this is where I'm having trouble
_invokeCallback(callback) {
callback();
this.randomInteger = 0;
}
render() {
const { onSomeEvent } = this.props;
// Error
return (
<button onClick={this._invokeCallback(onSomeEvent)} />
)
}
}
What I want from here is to reassign this.randomInteger to a certain value AFTER invoking the callback that was handed down from the parent component. What can I do from here?
I apologize if my example is missing some pieces or is incomplete. I am rushing to write this up. Please feel free to correct any mistakes I made in my phrasing or example code. Thanks in advance.
Your _invokeCallback is executing immediately.
Due to the parentheses and passing an argument here this._invokeCallback(onSomeEvent), you are setting onClick to the result of the _invokeCallback method.
This is what is causing the infinite loop where setState in the parent causes a re-render in the child which then executes _invokeCallback again, and so on.
You could use an anonymous function with onClick so that _invokeCallback is only executed when the button is clicked:
render() {
const { onSomeEvent } = this.props
return (
<button onClick={ () => this._invokeCallback(onSomeEvent) } />
)
}
Alternatively, you could call the onSomeEvent function from the _invokeCallback method:
_invokeCallback() {
this.props.onSomeEvent()
this.randomInteger = 0
}
render() {
return (
<button onClick={ this._invokeCallback } />
)
}