Recently, I have been working with a dynamic control of input text boxes, each with their own id's. However, I noticed that if I stored an array of elements and each element was an object with different variables and such, any change in on of those variables in the object would not alert React to update the children components(which receive the state through props). I am trying to control the input according to the documentation, but am confused as to why changes in the parent's state of a normal non array and non object variable will be recognized by the children as a change in props and yet not for normal state arrays. Here is my parent code:
import React, {Component} from 'react';
import Music from './music'
import axios from 'axios';
import update from 'immutability-helper';
class Selection {
constructor(){
this.music = '';
this.beginning = '';
this.the_end = '';
}
setTitle=(title)=>{
this.music = title;
}
setStart=(start)=>{
this.beginning = start;
}
setEnd=(end)=>{
this.the_end = end;
}
}
class Practice extends React.Component{
constructor(props){
super(props);
this.state = {
selections: Array(0),
test: 0,
}
this.removeSong = this.removeSong.bind(this);
}
removeSong(index){
var newArray = this.state.selections.slice();
newArray.splice(index, 1);
this.setState({selections: newArray, test: this.state.test+=1});
}
addAnotherSong=()=>{
var newArray = this.state.selections.slice();
newArray.push(new Selection());
this.setState({ selections: newArray, test: this.state.test+=1});
}
render(){
return(
<div>
<button onClick={() => this.props.practice()}>Log Practice Session</button>
<h1>{this.props.time}</h1>
<form >
Description: <input type="form" placeholder="How did it go?" name="fname"/><br/>
</form>
<button onClick={()=>this.addAnotherSong()}>Add Another Piece</button>
<button onClick={()=>this.setState({test: this.state.test})}>Will it now Update?</button>
{
this.state.selections.map((child, index) => (
this.state.selections[index].music,
<Music key={index} number={index} subtract={this.removeSong}
Title={this.state.test} Start={this.state.selections[index].beginning}
End={this.state.selections[index].the_end} changeTitle={this.state.selections[index].setTitle}
changeStart={this.state.selections[index].setStart} changeEnd={this.state.selections[index].setEnd}
/>
))
}
</div>
);
}
}
export default Practice;
As you can see, I have an array in the state with objects constructed from the Selection class. Here are the children that won't update unless the change happens in the non array type "this.state.test" prop.
import React, {Component} from 'react';
import InputBox from './input';
class Music extends React.Component{
constructor(props){
super(props);
}
shouldComponentUpdate(newProps){
if(this.props !== newProps){
return true;
} else{
console.log("wassup");
return false;
}
}
render(){
return(
<div>{this.props.number}
<InputBox cValue={this.props.Title} identity={this.props.number} updateInput={this.props.changeTitle} />
<InputBox cValue={this.props.Start} identity={this.props.number} updateInput={this.props.changeStart} />
<InputBox cValue={this.props.End} identity={this.props.number} updateInput={this.props.changeEnd} />
<button onClick={()=> this.props.subtract(this.props.number)}>DELETE{this.props.number}</button>
{this.props.Title}
</div>
)
}
}
export default Music;
Lastly, here is the children of that child.
import React,{Component} from 'react';
class InputBox extends React.Component{
constructor(props){
super(props);
this.state = { value: '' }
this.handleChange = this.handleChange.bind(this);
}
handleChange(event){
this.setState({value: event.target.value});
this.props.updateInput(this.state.value, this.props.identity);
console.log("test" + this.props.cValue);
}
shouldComponentUpdate(newProps){
if(this.props !== newProps){
console.log("Updating Input Component");
return true;
} else {
console.log("yo");
return false;
}
}
render(){
return(
<input type="text"onChange={this.handleChange} value={this.props.cValue}/>
)
}
}
export default InputBox;
If this is a bad question, please let me know. But, any help is always appreciated. Thankyou
You're treating your state as a mutable object - states in React should never be mutated, and a state change should always be a brand new object.
Take, for example, we have just one Selection in state.selections:
[ { music: 'music', beginning: 'beginning', the_end: 'ending' } ]
If the Music component ends up calling setStart('newBeginning'), then we end up with this in our state:
[ { music: 'music', beginning: 'newBeginning', the_end: 'ending' } ]
This looks like everything went well, but React won't actually pick up this change by default because state.selections is still referring to the same array.
To fix it, you're going to have to figure out how to update your array reference while you modify individual items. You've already done this in removeSong and addAnotherSong by calling slice on your current state. Slice returns a new array, so no issues there.
I'd recommend having methods to modify individual Selections in Practice, as opposed to each Selection modifying itself.
Something like this could work:
class Practice extends React.Component{
updateSelectionBeginning(index, newBeginning){
var newArray = this.state.selections.slice();
newArray[index].beginning = newBeginning;
this.setState({selections: newArray});
}
}
We'd then pass it down to our children via props, and they'd call updateSelectionBeginning(FOO, BAR). That lets us update the state, while also keeping it immutable.
Related
I'm working on a React SPA and trying to render JSON data with a filter. I have several containers that each have a class value. When clicking on a container, I am trying to pass a value to a setState. When stepping out of the map function, however, I cannot get the new state change to update.
I'm assuming that the render state isn't getting updated for the state.
import React, { Component } from "react";
import ListData from "../data/list.json";
class Levels extends Component {
constructor(props) {
super(props);
this.state = { newFilter: "" };
this.onClick = this.setFilter.bind(this);
}
setFilter = e => {
console.log(e); //This returns the correct class.
this.setState({ newFilter: e }); //This will not update
};
render() {
console.log(this.state.newFilter); //This just returns the initial state of ''
const activeTab = this.props.keyNumber;
let levelFilter = ListData.filter(i => {
return i.level === activeTab;
});
let renderLevel = levelFilter.map((detail, i) => {
let short_desc;
if ((detail.short || []).length === 0) {
short_desc = "";
} else {
short_desc = <p>{detail.short}</p>;
}
return (
<div
className={`kr_sItem ${detail.class}`}
data-value={detail.class}
onClick={() => this.onClick(detail.class)}
value={detail.class}
key={i}
>
<h1>{detail.title}</h1>
{short_desc}
<img src={`${detail.img}`} />
</div>
);
});
console.log(this.state.newFilter);
return (
<div id="kr_app_wrapper">
<div id="lOneWrapper">{renderLevel}</div>
<div id="lTwoWrapper"></div>
</div>
);
}
}
export default Levels;
Here is the code calling in the Component:
import React, {Component} from 'react';
import Levels from './components/levels2';
import {
Route,
NavLink,
HashRouter
} from "react-router-dom";
import LevelTwo from "./levelTwo";
class LevelOne extends Component{
render(){
return(
<div id="lOneWrapper">
<NavLink to='/Level2'><Levels keyNumber={1} /></NavLink>
</div>
)
}
}
export default LevelOne;
Edit: As pointed out in the comments, binding an arrow function is pointless but it wouldn't cause the error here, so there must be something else going on in some other part of your code.
The problem is you are trying to bind an arrow function that doesn't have an implicit this. Either call setFilter directly (no need to bind this with arrow functions) or change your setFilter function to a regular function:
class Levels extends Component{
constructor(props){
super(props);
this.state = {newFilter: ''};
this.onClick = this.setFilter.bind(this);
}
setFilter(e) {
console.log(e); //This returns the correct class.
this.setState({newFilter: e}); //This will not update
}
this.state.onlyvar.TheName is showing up as undefined and I don't know why.
import React, { Component } from 'react';
class Thevar extends Component {
constructor(props) {
super(props);
this.state = {
onlyvar: [{
TheName:"getter Name"
}]
}
}
render() {
return (
<div>
TheOnlyVar: <p>{console.log(this.state.onlyvar.TheName)}</p>
</div>
)
}
}
export default Thevar;
In your code there are two mistakes:
onlyvar is an array, to access its items you should use an index to specify which item you want, in this case, the first (and only) item.
console.log() prints the value in the console, not on the page.
Here is the corrected code:
return (
<div>
TheOnlyVar: <p>{this.state.onlyvar[0].TheName}</p>
</div>
)
I'm in the React work by 2 hours and I have a problem with how the writer separate correctly the component, an example I have these windows
When I click the button "Set" I change the value this.state.nameFramework, If I write all code inside the App component my helloApp work but if I write the code in the separate component it not work in the instant time but for change the value of the variable this.state.nameframework I had reloaded the page.
My code
require('normalize.css/normalize.css');
require('styles/App.css');
import React from 'react';
import InputFramework from 'components/InputFramework';
import ListPerson from 'components/ListPerson';
const list = [
{
objectID: 1,
'name': 'Vincenzo',
'surname': 'Palazzo'
},
{
objectID: 2,
'name': 'Sara',
'surname': 'Durante'
}
];
let name = 'Vincent';
let nameFramework = 'React';
class AppComponent extends React.Component {
constructor(props){
super(props);
this.state = {
list,
name,
nameFramework
};
this.onSelectPerson = this.onSelectPerson.bind(this);
this.onSubmitText = this.onSubmitText.bind(this);
this.onChangeNameFramework = this.onChangeNameFramework.bind(this);
}
onSubmitText(){
this.setState({nameFramework: this.state.nameFramework});
}
onChangeNameFramework(name){
this.state.nameFramework = name;
}
onSelectPerson(name) {
this.setState({name: name});
}
render() {
//This is no good for my programmer style, resolve it please
return (
<div className="index">
<InputFramework
name={this.state.name}
nameFramework={this.state.nameFramework}
onChange={this.onChangeNameFramework}
onClick={this.onSubmitText}
/>
<ListPerson
onClick={this.onSelectPerson}
list={this.state.list}/>
</div>
);
}
}
AppComponent.defaultProps = {
};
export default AppComponent;
Input component
require('normalize.css/normalize.css');
require('styles/App.css');
import React from 'react';
class InputFramework extends React.Component {
constructor(props){
super(props);
}
render() {
//This is no good for my programmer style, resolve it please
//The nameFramework not update
let {onChange, onClick, name} = this.props;
return (
<div className='index'>
<h1>Hello my name is {name} and I'm learning {this.props.nameFramework}</h1>
<from>
<input type='text'
onChange={event => onChange(event.target.value)}/>
<button type='submit' onClick={() => onClick}>Set</button>
</from>
</div>
);
}
}
InputFramework.defaultProps = {};
export default InputFramework;
List component
require('normalize.css/normalize.css');
require('styles/App.css');
import React from 'react';
class ListPerson extends React.Component {
constructor(props){
super(props);
}
render() {
//This is no good for my programmer style, resolve it please
const {onClick, list} = this.props;
return (
<div className="index">
<ul>
{list.map(function(item){
return (
<li key={item.objectID}>
{item.name}
<button type='button' onClick={() => onClick(item.name)}>Select</button>
</li>
)
})}
</ul>
</div>
);
}
}
ListPerson.defaultProps = {
};
export default ListPerson;
I this is a problem to how to write the code, now I ask you that you have more experience than me, can you help me to undestend.
You are trying to change (mutate) state directly in onChangeNameFramework handler.
Mutating state directly can lead to bugs.
State must be changed only by this.setState, so it must be like this:
onChangeNameFramework(name){
this.setState({
nameFramework: name
})
}
Here is the docs:
https://reactjs.org/docs/state-and-lifecycle.html#do-not-modify-state-directly
Another problem is in InputFramework component, when you submit a form the page reloads, to prevent it, you should add e.preventDefault() like this:
class InputFramework extends React.Component {
render() {
//This is no good for my programmer style, resolve it please
//The nameFramework not update
let {onChange, onClick, name} = this.props;
const handleClick = (e) => {
e.preventDefault();
onClick();
}
return (
<div className='index'>
<h1>Hello my name is {name} and I'm learning {this.props.nameFramework}</h1>
<form>
<input type='text'
onChange={event => onChange(event.target.value)}/>
<button type='submit' onClick={handleClick}>Set</button>
</form>
</div>
);
}
}
Lastly in AppComponent the following code is redundant, since you are setting the same state:
onSubmitText(){
this.setState({nameFramework: this.state.nameFramework});
}
You already handle the change of framework name in onChangeNameFramework handler.
I think using both onSubmitText and onChangeNameFramework handler seems unnecesary here, only one of them will be enough.
Playground:
https://codesandbox.io/s/blue-frost-qutb0
I am learning React and I am trying to call a function in a child component, that accesses a property that was passed from parent component and display it.
The props receives a "todo" object that has 2 properties, one of them is text.
I have tried to display the text directly without a function, like {this.props.todo.text} but it does not appear. I also tried like the code shows, by calling a function that returns the text.
This is my App.js
import React, { Component } from "react";
import NavBar from "./components/NavBar";
import "./App.css";
import TodoList from "./components/todoList";
import TodoElement from "./components/todoElement";
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
this.addNewTodo = this.addNewTodo.bind(this);
}
addNewTodo(input) {
const newTodo = {
text: input,
done: false
};
const todos = [...this.state.todos];
todos.push(newTodo);
this.setState({ todos });
}
render() {
return (
<div className="App">
<input type="text" id="text" />
<button
onClick={() => this.addNewTodo(document.getElementById("text"))}
>
Add new
</button>
{this.state.todos.map(todo => (
<TodoElement key={todo.text} todo={todo} />
))}
</div>
);
}
}
export default App;
This is my todoElement.jsx
import React, { Component } from "react";
class TodoElement extends Component {
state = {};
writeText() {
const texto = this.props.todo.text;
return texto;
}
render() {
return (
<div className="row">
<input type="checkbox" />
<p id={this.writeText()>{this.writeText()}</p>
<button>x</button>
</div>
);
}
}
export default TodoElement;
I expect that when I write in the input box, and press add, it will display the text.
From documentation
Refs provide a way to access DOM nodes or React elements created in the render method.
I'll write it as:
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
this.textRef = React.createRef();
this.addNewTodo = this.addNewTodo.bind(this);
}
addNewTodo() {
const newTodo = {
text: this.textRef.current.value,
done: false
};
const todos = [...this.state.todos, newTodo];
this.setState({ todos });
}
render() {
return (
<div className="App">
<input type="text" id="text" ref={this.textRef} />
<button onClick={this.addNewTodo}>Add new</button>
{this.state.todos.map(todo => (
<TodoElement key={todo.text} todo={todo} />
))}
</div>
);
}
}
In your approach, what you got as an argument to the parameter input of the method addNewTodo is an Element object. It is not the value you entered into the text field. To get the value, you need to call input.value. But this is approach is not we encourage in React, rather we use Ref when need to access the html native dom.
I have a few components that each contain inputs, and on the main component I have a button that will send that information all at once to the server. The problem is that the main component that has the button doesn't have the input content of the child components.
In the past I've passed down a method that would send the content back up into the state, but is there an easier less painful way of doing it? It just feels like an odd way of doing that.
Here's a short example of what I have and what I mean.
Main component:
import React from 'react';
import { Button } from 'react-toolbox/lib/button';
import Message from './Message';
class Main extends React.Component {
constructor() {
super();
this.state = { test: '' };
}
render() {
return (
<div className="container mainFrame">
<h2>Program</h2>
<Message />
</div>
);
}
}
export default Main;
And the message component:
import React from 'react';
import axios from 'axios';
import Input from 'react-toolbox/lib/input';
class Message extends React.Component {
constructor() {
super();
this.state = { message: '' };
this.handleChange = this.handleChange.bind(this);
}
handleChange(value) {
this.setState({ message: value });
}
render() {
return (
<Input
type="text"
label="Message"
name="name"
onChange={this.handleChange}
/>
);
}
}
export default Message;
To answer your question, yes. You can try using refs. Add a ref to Message component, and you will be able to access the child component's methods, state and everything. But thats not the conventional way, people generally use callbacks, as you mentioned earlier.
import React from 'react';
import { Button } from 'react-toolbox/lib/button';
import Message from './Message';
class Main extends React.Component {
constructor() {
super();
this.state = { test: '' };
}
clickHandler () {
let childState = this.refs.comp1.state //returns the child's state. not prefered.
let childValue = this.refs.comp1.getValue(); // calling a method that returns the child's value
}
render() {
return (
<div className="container mainFrame">
<h2>Program</h2>
<Message ref="comp1"/>
<Button onClick={this.clickHandler} />
</div>
);
}
}
export default Main;
import React from 'react';
import axios from 'axios';
import Input from 'react-toolbox/lib/input';
class Message extends React.Component {
constructor() {
super();
this.state = { message: '' };
this.handleChange = this.handleChange.bind(this);
}
handleChange(value) {
this.setState({ message: value });
}
getValue () {
return this.state.message;
}
render() {
return (
<Input
type="text"
label="Message"
name="name"
onChange={this.handleChange}
/>
);
}
}
export default Message;
You are doing what is suggested in docs so it's a good way.
I have a button that will send that information all at once to the server
I assume then it might be form you can use. If so you can just handle onSubmit event and create FormData object containing all nested input field names with their values (even in children components). No need for callbacks then.
handleSubmit(e){
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form); // send it as a body of your request
// form data object will contain key value pairs corresponding to input `name`s and their values.
}
checkout Retrieving a FormData object from an HTML form