I'm making a very basic React app from teamtreehouse.com, and I'm constantly encountering
"TypeError: Cannot read property 'onPlayerScoreChange' of undefined"
even though I'm binding my functions correctly (I think)
'onPlayerScoreChange' Is a method in the Grandparent component which executes when a user hits a '+' or '-' button to change a player's score.
It would be really helpful if someone could explain what is wrong, because I think I am setting this.onPlayerScoreChange = this.onPlayerScoreChange.bind(this) in the great grandparent's constructor.
Parent component:
class App extends React.Component {
constructor(props) {
super(props);
this.onPlayerScoreChange = this.onPlayerScoreChange.bind(this)
this.state = {
initialPlayers: props.initialPlayers,
};
}
onPlayerScoreChange(delta, index) {
this.setState((prevState, props) => {
return {initialPlayers: this.prevState.initialPlayers[index].score += delta}
})
}
render() {
return(
<div className = "scoreboard">
<Header title = {this.props.title}/>
<div className = "players">
{this.state.initialPlayers.map(function(player, index) {
return(
<Player
name = {player.name}
score = {player.score}
key = {player.id}
index = {index}
onScoreChange = {this.onPlayerScoreChange}
/>
)
})}
</div>
</div>
)
}}
(Component has default props for title)
Child component:
class Player extends React.Component {
render() {
return(
<div className = "player">
<div className = "player-name">
{this.props.name}
</div>
<div className = "player-score">
<Counter score = {this.props.score} onChange = {this.props.onScoreChange} index = {this.props.index}/>
</div>
</div>
)
}}
Grandchild component:
class Counter extends React.Component {
constructor(props) {
super(props)
this.handleDecrement = this.handleDecrement.bind(this)
this.handleIncrement = this.handleIncrement.bind(this)
}
handleDecrement() {
this.props.onChange(-1, this.props.index)
}
handleIncrement() {
this.props.onChange(1, this.props.index)
}
render() {
return(
<div className = "counter">
<button className = "counter-action decrement" onClick = {this.handleDecrement}> - </button>
<div className = "counter-score"> {this.props.score} </div>
<button className = "counter-action increment" onClick = {this.handleIncrement}> + </button>
</div>
)}}
Thank you!
You have not done binding for the map function where you are using onScoreChange = {this.onPlayerScoreChange},
you can either use bind or arrow functions for binding
P.S. Binding is needed because the context of the map function is different from the React Component context and hence this inside this function won't be Referring to the React Components this and thus you can't access that properties of the React Component class.
With Arrow function:
{this.state.initialPlayers.map((player, index)=> {
return(
<Player
name = {player.name}
score = {player.score}
key = {player.id}
index = {index}
onScoreChange = {this.onPlayerScoreChange}
/>
)
})}
With bind
{this.state.initialPlayers.map(function(player, index) {
return(
<Player
name = {player.name}
score = {player.score}
key = {player.id}
index = {index}
onScoreChange = {this.onPlayerScoreChange}
/>
)
}.bind(this))}
Can also be done by passing second argument as this to map function as onClick event uses local this of map function which is undefined here and this currently refers to the global object.
{this.state.initialPlayers.map(function(player, index) {
return(
<Player
name = {player.name}
score = {player.score}
key = {player.id}
index = {index}
onScoreChange = {this.onPlayerScoreChange}
/>
)
}),this}
sometimes it is quite easy and it all depends on how you declare your loop
example you will get this error if you try to do something like this var.map(function(example, index) {}
but if you call the new function within the map with this
this.sate.articles.map(list =>
{<a onClick={() => this.myNewfunction()}>{list.title}</a>)}
the second loop will get you out of the undefined error
and dont forget to bind your new function
//This should be declared befor map function
const thObj = this;
this.sate.articles.map(list =>
{<a onClick={() => thObj.myNewfunction()}>{list.title})}
Related
Is there a native way to add a style class name to a react element passed as a property WITHOUT using jQuery or any 3rd-party libraries.
The following example should demonstrate what I'm trying to do. Note, react class names are made up.
Edit: The point is to modify the class name of a react element that is passes as a property to the Books class! The Books class needs to modify the class name. Apparently, it does not have access to Authors class's state to use within Authors class.
File authors.js
class Authors {
render() {
return (
<ul>
<li>John Doe</li>
<li>Jane Doe</li>
</ul>
);
}
}
File shelf.js
class Shelf {
render() {
return (
<div>
<Books authors={<Authors/>}/>
</div>
);
}
}
File books.js
class Books {
this.props.authors.addClass('style-class-name'); <- HERE
render() {
return (
<div>
{this.props.authors}
</div>
);
}
}
Potentially need more context, but in this kind of scenario, I would use state to dynamically add/remove a class. A basic example would be:
const App = () => {
const [dynamicClass, setDynamicClass] = useState("");
return (
<div className={`normalClass ${dynamicClass}`}>
<button onClick={() => setDynamicClass("red")}>Red</button>
<button onClick={() => setDynamicClass("green")}>Green</button>
<button onClick={() => setDynamicClass("")}>Reset</button>
</div>
);
};
The state changes schedule a re-render, hence you end up with dynamic classes depending on the state. Could also pass the class in as a property, or however you want to inject it into the component.
React elements do have an attribute called className. You can use that to set CSS classes to your component. You can pass static data (strings) or dynamic ones (basically calculated ones):
<div className="fixedValue" />
<div className={fromThisVariable} />
Keep in mind, that you have to pass down your className, if you wrap native HTML elements in a component:
class Books {
render() {
const {
authors,
// other represents all attributes, that are not 'authors'
...other
}
return (
<div {...other}>
{this.props.authors}
</div>
);
}
}
If you want to add data to your authors attribute (which I assume is an array), you could implement a thing like the following:
let configuredAuthors = this.props.authors.map((author) => ({
return {
...author,
className: `${author.firstName}-${author.lastName}`
}
}))
Keep in mind, that either way, you have to manually assign this className property in your child components (I guess an Author component)
To handle updates from a child component, use functions: https://reactjs.org/docs/faq-functions.html
Full example:
import React from "react";
class Shelf extends React.Component {
render() {
const authors = [
{
firstName: "John",
lastName: "Tolkien"
},
{
firstName: "Stephen",
lastName: "King"
}
];
return (
<div>
<Books authors={authors} />
</div>
);
}
}
const Books = ({authors, ...other}) => {
const [configuredAuthors, setAuthors] = React.useState(authors)
const updateClassName = (authorIndex, newClassName) => {
const newAuthors = [...configuredAuthors]
newAuthors[authorIndex] = {
...configuredAuthors[authorIndex],
className: newClassName
}
setAuthors(newAuthors)
}
return (
<ul {...other}>
{configuredAuthors.map((author, index) => {
return <Author key={index} author={author}
index={index}
updateClassName={updateClassName}
/>;
})}
</ul>
);
}
const Author = ({ author, index, updateClassName, ...other }) => {
const [count, setCount] = React.useState(0);
return (
<li className={author.className} {...other}>
<span>{`${author.firstName} ${author.lastName}`}</span>
<button
onClick={() => {
updateClassName(index, `author-${count}`);
setCount(count + 1);
}}
>
update Class ()
{`current: ${author.className || '<none>'}`}
</button>
</li>
);
};
export default function App() {
return <Shelf />;
}
This is my parent Component having state ( value and item ). I am trying to pass value state as a props to child component. The code executed in render method is Performing toggle when i click on button. But when i call the list function inside componentDidMount, Toggle is not working but click event is performed.
import React, { Component } from 'react'
import Card from './Components/Card/Card'
export class App extends Component {
state = {
values : new Array(4).fill(false),
item : [],
}
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = this.state.values;
stateObject.splice(index,1,!this.state.values[index]);
this.setState({ values: stateObject });
}
list = () => {
const listItem = this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
this.setState({ item : listItem });
}
componentDidMount(){
// if this is not executed as the JSX is render method is executed everything is working fine. as props are getting update in child component.
this.list();
}
render() {
return (
<div>
{/* {this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})
} */}
{this.state.item}
</div>
)
}
}
export default App
This is my child Component where the state is passed as props
import React from 'react'
const Card = (props) => {
return (
<div>
<section>
<h1>Name : John Doe</h1>
<h3>Age : 20 </h3>
</section>
{props.show ?
<section>Skills : good at nothing</section> : null
}
<button onClick={props.toggleHandler} >Toggle</button>
</div>
)
}
export default Card
I know the componentDidMount is executed only once. but how to make it work except writing the JSX directly inside render method
make a copy of the state instead of mutating it directly. By using [...this.state.values] or this.state.values.slice()
toggleHandler = (index) => {
console.log("CLICKED");
let stateObject = [...this.state.values]
stateObject = stateObject.filter((_, i) => i !== index);
this.setState({ values: stateObject });
}
Also in your render method, this.state.item is an array so you need to loop it
{this.state.item.map(Element => <Element />}
Also directly in your Render method you can just do
{this.state.values.map((data, index) => {
return <Card key = {index}
show = {this.state.values[index]}
toggleHandler = {() => this.toggleHandler(index)} />
})}
In your card component try using
<button onClick={() => props.toggleHandler()}} >Toggle</button>
Value should be mapped inside render() of the class component in order to work
like this:
render() {
const { values } = this.state;
return (
<div>
{values.map((data, index) => {
return (
<Card
key={index}
show={values[index]}
toggleHandler={() => this.toggleHandler(index)}
/>
);
})}
</div>
);
}
check sandbox for demo
https://codesandbox.io/s/stupefied-spence-67p4f?file=/src/App.js
My code is
class Com extends React.Component {
constructor(props) {
super(props);
this.state = {is_clicked: false};
}
render() {
let sub_com1 = () => {
return (
<div>Input1:<input/></div>
);
};
let sub_com2 = () => {
return (
<div>Input2:<input/></div>
);
};
return (
<div>
<div>
{this.state.is_clicked ? sub_com1() : sub_com2()}
</div>
<button onClick={()=>{
let is_clicked=this.state.is_clicked;
this.setState({is_clicked: !is_clicked});
}}>
Click me
</button>
</div>
);
}
}
and the live display: codepen.
In this code, I use conditional rendering in Com's render method.
What I expect
Each time I click the button, the input area should be cleared since it is rendered to another component
What I met
Each time I click the button, the "input1" or "input2" label has changed, but the input area is not cleared.
To fix this issue you have to add key attributes to your input elements, change the code be like this and it will work:
class Com extends React.Component {
constructor(props) {
super(props);
this.state = {is_clicked: false};
}
render() {
let sub_com1 = () => {
return (
<div>Input1:<input key={1} id='A' /></div>
);
};
let sub_com2 = () => {
return (
<div>Input2:<input key={2} id='b' /></div>
);
};
return (
<div>
<div>
{this.state.is_clicked ? sub_com1() : sub_com2()}
</div>
<button onClick={()=>{
let is_clicked=this.state.is_clicked;
this.setState({is_clicked: !is_clicked});
}}>
Click me
</button>
</div>
);
}
}
ReactDOM.render(
<Com/>,
mountNode,
);
The following article discuss it in more depth and why its important to have key attribute for elements:
full article: keys-in-children-components-are-important
Key is not really about performance, it’s more about identity (which
in turn leads to better performance). Randomly assigned and changing
values do not form an identity Paul O’Shannessy
I am sharing an event in App comp between two child components
App Comp
class App extends Component {
constructor(props) {
super(props);
this.state = { A : 'good' };
}
SharedEvent () {
var newvalue = this.setState({A:'update'}, () => {
console.log(this.state);
alert(this.state.A)
});
}
render() {
return (
<div className="App">
<Content child1Event = {this.SharedEvent} text = {this.state.A}
child2Event = {this.SharedEvent} />
</div>
);
}
}
Parent comp
render(props) {
return (
<div className = "App">
<Subcontent whatever = {this.props.child1Event} name = {this.props.text} />
<Subcontent2 whatever = {this.props.child2Event} />
</div>
);
}
}
Child Comp
constructor(props) {
super(props);
this.state = { };
}
render() {
return (
<button id = 'btn' onClick = {this.props.whatever.bind(this , 'shared')} > {this.props.name} </button>
);
}
}
subcontent2 is same as subontent
I can successfully trigger sharedEvent from both components but it should change the name of button on setstate which it does not where am i wrong ???
the problem can be from one of these two issues:
First, you should replace your SharedEvent(){} function with SharedEvent = ()=>{...you function code} and it's because the scope has changed and if you are referring to this in your component for calling one of its functions, you should either use arrow functions or define them like you have done and bind them in your constructor to this which you have not done.
Second, the onClick event on button, restarts the page by its default behavior and everything refreshes as does your component state, and this might be the cause that you do not see the text change because the page refreshes and the state gets back to 'good', so try replacing your onClick function with this:
<button onClick={e => {e.preventDefault(); this.props.whatever();}}> {this.props.name} </button>
so I wanted to have a component iterate through an object within it's state and pass the data down to it's child. My parent component looks basically like this:
class ListContainer extends React.Component{
constructor(props){
super(props);
this.state = {"stuff": {
"pie": ["bread", "apple"],
"fries": ["potatoes", "oil"]
}
};
render(){
let rendArr = [];
for(recipe in this.state.stuff){
let newRecipe = <Child tableName={recipe} recipeData={this.state.stuff[recipe]} />;
rendArr.push(newRecipe);
}
return(
<div id="11"> I work
{rendArr}
</div>
);
}
However, I get an error saying that the "recipe" placeholder I used in the for loop isn't defined. I'm guessing I'm using the for loop here wrong with JSX, but I don't know the right way to iterate through an object. I know I could probably just convert it into an array of objects or something, but right now I'd like to understand why this for loop doesn't work in React.
In ReactJS: typical practice is to render lists using Array.prototype.map().
Object.entries() and Destructuring Assignment can be combined to reach a convenient form.
See below for a practical example.
// List.
class List extends React.Component {
// State.
state = {
recipes: {
pie: ['bread', 'apple'],
fries: ['potatoes', 'oil']
}
}
// Render.
render = () => (
<div id="11">
<h1>Map</h1>
{this.map()}
<h1>For Loop</h1>
{this.forLoop()}
</div>
)
// Map.
map = () => Object.entries(this.state.recipes).map(([name, recipe]) => <Recipe key={name} name={name} recipe={recipe}/>)
// For Loop.
forLoop = () => {
const list = []
for (let name in this.state.recipes) {
const recipe = this.state.recipes[name]
const line = <Recipe key={name} name={name} recipe={recipe}/>
list.push(line)
}
return list
}
}
// Recipe.
const Recipe = ({name, recipe}) => (
<div>
<h3 style={{textTransform: 'capitalize'}}>{name}</h3>
{recipe.map(ingredient => <div>{ingredient}</div>)}
</div>
)
ReactDOM.render(<List/>, document.querySelector('#root'))
<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="root"></div>
Instead of pushing component in an array, push the object and pull that in the render using map method:
render(){
let rendArr = [];
for(recipe in this.state.stuff){
rendArr.push({tn: recipe, rd: this.state.stuff[recipe]});
}
return(
<div id="11"> I work
{
rendArr.map(el=> {
<Child tableName={el.tn} recipeData={el.rd} />
})
}
</div>
);
}
use map instead of for..in loop:
render(){
const rendArr = this.state.stuff.map((recipe, i) => <Child
key={i}
tableName={recipe}
recipeData={recipe}
/>);
return(
<div id="11"> I work
{rendArr}
</div>
);
}