React is not changing the state of the parent element - javascript

I'm building simple todo app in react and I have made input field as part of inputForm element which is child element.
I can pass functions as props from parent to child without problem, but I can't update parent state to store value on input field. When I type in input field, passed function is executing normally but currentTodo state is not updating.
I have found that this problem can be avoided by using single data flow pattern (like Flux or Reflux) but as this is my first project I want to understand how to work with basics.
Code for parent element:
import React, { Component } from 'react';
import './App.css';
import InputForm from '../components/InputForm'
import {Task} from '../components/Task'
class App extends Component {
constructor(){
super();
this.state = {
tasks: ["Todo", "Toda"],
currentToDo: "",
};
}
//makes copy of task array, pushes current to do to copy and setsState
//with new values
addTodo = () => {
console.log("addTodo")
let copy = this.state.tasks.slice();
console.log(this.state.currentToDo)
copy.push(this.state.currentToDo);
this.setState({tasks: copy});
}
//gets input value from input field and updates current todo
onInputChange = e => {
console.log(e.target.value);
this.setState({ currentTodo: e.target.value })
}
render() {
let drawTask = this.state.tasks.map(e => {
return <Task todo={e}/>
})
return (
<div className="container">
<InputForm onInputChange={() => this.onInputChange} add={this.addTodo}/>
{drawTask}
</div>
);
}
}
export default App;
Code for child element:
import React, { Component } from 'react';
import './component.css';
import {AddButton} from './Buttons.js'
class InputForm extends Component{
constructor(){
super();
this.state = {
}
}
render(){
return(
<div className='taskHeader'>
{/*Value of current todo is send as props from parent element*/}
<input value = {this.props.currentToDo} onChange={this.props.onInputChange()} type="text"/>
<AddButton add = {this.props.add}/>
</div>
)
}
}
export default InputForm;

You are calling the function during the render rather than passing a reference.
Parent owns the function and needs to pass it to the child:
<InputForm onInputChange={this.onInputChange} add={this.addTodo}/>
Now that the child has a prop called onInputChange, you pass it to the onChange callback as a reference.
<input value={this.props.currentToDo} onChange={this.props.onInputChange} type="text"/>

Related

Why doesn't my React child component get value (re-render) when I change the parent state?

Background
I wrote an exact, short yet complete example of a Parent component with a nested Child component which simply attempts:
Alter a string in the Parent's state
See the Child component updated when the Parent's state value is altered (this.state.name)
Here's What It Looks Like
When the app loads a default value is passed from Parent state to child props.
Change The Name
All I want to do is allow the change of the name after the user adds a new name in the Parent's <input> and clicks the Parent's <button>
However, as you can see, when the user clicks the button only the Parent is rendered again.
Questions
Is it possible to get the Child to render the new value?
What am i doing wrong in this example -- why isn't it updating or
rendering the new value?
All Source Code
Here is all of the source code and you can view it and try it in my StackBlitz project.
I've kept it as simple as possible.
Parent component (DataLoader)
import * as React from 'react';
import { useState } from 'react';
import { Grid } from './Grid.tsx';
interface LoaderProps {
name: string;
}
export class DataLoader extends React.Component<LoaderProps, {}> {
state: any = {};
constructor(props: LoaderProps) {
super(props);
this.state.name = this.props.name;
this.changeName = this.changeName.bind(this);
}
render() {
const { name } = this.state;
let parentOutput = <span>{name}</span>;
return (
<div>
<button onClick={this.changeName}>Change Name</button>
<input id="mapvalue" type="text" placeholder="name" />
<hr id="parent" />
<div>### Parent ###</div>
<strong>Name</strong>: {parentOutput}
<hr id="child" />
<Grid childName={name} />
</div>
);
}
changeName() {
let newValue = document.querySelector('#mapvalue').value.toString();
console.log(newValue);
this.setState({
name: newValue,
});
}
}
Child component (Grid)
import * as React from 'react';
interface PropsParams {
childName: string;
}
export class Grid extends React.Component<PropsParams, {}> {
state: any = {};
constructor(props: PropsParams) {
super(props);
let counter = 0;
this.state = { childName: this.props.childName };
console.log(`CHILD -> this.state.name : ${this.state.childName}`);
}
render() {
const { childName } = this.state;
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
App.tsx is set up like the following -- this is where default value comes in on props
import * as React from 'react';
import { DataLoader } from './DataLoader.tsx';
import './style.css';
export default function App() {
return (
<div>
<DataLoader name={'default value'} />
</div>
);
}
You're seeing two different values because you're tracking two different states. One in the parent component and one in the child component.
Don't duplicate data.
If the child component should always display the prop that's passed to it then don't track state in the child component, just display the prop that's passed to it. For example:
export class Grid extends React.Component<PropsParams, {}> {
render() {
const { childName } = this.props; // <--- read the value from props, not local state
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
In the Child component, you set the prop childName value to state in the contructor ONLY. The constructor is executed ONLY WHEN THE COMPONENT IS MOUNTED. So, it doesn't know if the childName prop is changed later.
There are 2 solutions for this.
(1) Directly use this.props.childName without setting it to a state.
(2) Add a useEffect that updates the state value on prop change.
React.useEffect(() => {
this.state = {
childName: this.props.childName;
};
}, [this.props.childName]);
However, I recommend 1st solution since it's not a good practice to duplicate data.

Component with form keeps refreshing page in ReactJS

I'm attempting at creating a simple todo app to try to cement some concepts.
This app gets some previous todos from a .js file with json object. And every time they get clicked they are deleted from the current app.
Now I wanted to add the ability to add todos, first to the current instance of app itself and afterwards to the file to ensure continuity.
My problem arises adding to the app instance.
When using the component with a form, it all goes south.
I tried putting all the component parts in the App.js main file but still the same result, it refreshes after the alert(value) line.
I've also tried changing the order inside the addTodo function and the alert only works if it's the first line of the function, anywhere else the refresh happens before it. So I assumed it's something about using the state of the app component?
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import todosData from "./todosData"
import TodoItem from "./TodoItem"
import TodoForm from './TodoForm'
class App extends Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
const allTodos = this.state.todos
const filtered = allTodos.filter(x => x.id !== id)
const filtered2 = filtered
filtered2.push({id:4,text:"HAKUNA MATATA",completed:false })
this.setState({todos:filtered2})
//added testing so that whenever I delete a todo it adds one with "HAKUNA MATATA"
}
addTodo(value) {
alert(value)
const allTodos = this.state.todos
const lastTodo = this.state.todos.slice(-1)[0]
const newId = lastTodo.id + 1
const newTodo = {id: newId, text: value, completed:false}
allTodos.push(newTodo)
this.setState({todos:allTodos})
}
render() {
const todoItems = this.state.todos.map( item =>
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
/>
)
const text = ""
return (
<div className="todo-list">
{todoItems}
<TodoForm addTodo={this.addTodo}/>
</div>
);
}
}
export default App;
TodoForm.js
import React from 'react'
class TodoForm extends React.Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(event) {
this.props.addTodo(this.input.value)
event.preventDefault()
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
</label>
<input type="text" ref={(input) => this.input = input}/>
</form>
)
}
}
export default TodoForm
Can you guys help me with this? From what I understood preventDefault was supposed to prevent this?
Call the preventDefault method on the event the first thing you do in handleSubmit and it should work.
handleSubmit(event) {
event.preventDefault()
this.props.addTodo(this.input.value)
}
You also need to bind the addTodo method to this in the App constructor.
class App extends Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
this.addTodo = this.addTodo.bind(this)
}
// ...
}
In case anyone comes to this answer from Google:
In my case, when I submitted the form, I lazy loaded a component underneath, but it had not been wrapped in <Suspense>. Adding that fixed my issue.

Uncaught RangeError Maximum call stack size exceeded in React App

I'm learning React and for training, I want to create a basic Todo app. For the first step, I want to create a component called AddTodo that renders an input field and a button and every time I enter something in the input field and press the button, I want to pass the value of the input field to another component called TodoList and append it to the list.
The problem is when I launch the app, the AddTodo component renders successfully but when I enter something and press the button, the app stops responding for 2 seconds and after that, I get this: Uncaught RangeError: Maximum call stack size exceeded and nothing happens.
My app source code: Main.jsx
import React, {Component} from 'react';
import TodoList from 'TodoList';
import AddTodo from 'AddTodo';
class Main extends Component {
constructor(props) {
super(props);
this.setNewTodo = this.setNewTodo.bind(this);
this.state = {
newTodo: ''
};
}
setNewTodo(todo) {
this.setState({
newTodo: todo
});
}
render() {
var {newTodo} = this.state;
return (
<div>
<TodoList addToList={newTodo} />
<AddTodo setTodo={this.setNewTodo}/>
</div>
);
}
}
export default Main;
AddTodo.jsx
import React, {Component} from 'react';
class AddTodo extends Component {
constructor(props) {
super(props);
this.handleNewTodo = this.handleNewTodo.bind(this);
}
handleNewTodo() {
var todo = this.refs.todo.value;
this.refs.todo.value = '';
if (todo) {
this.props.setTodo(todo);
}
}
render() {
return (
<div>
<input type="text" ref="todo" />
<button onClick={this.handleNewTodo}>Add to Todo List</button>
</div>
);
}
}
AddTodo.propTypes = {
setTodo: React.PropTypes.func.isRequired
};
export default AddTodo;
TodoList.jsx
import React, {Component} from 'react';
class TodoList extends Component {
constructor(props) {
super(props);
this.renderItems = this.renderItems.bind(this);
this.state = {
todos: []
};
}
componentDidUpdate() {
var newTodo = this.props.addToList;
var todos = this.state.todos;
todos = todos.concat(newTodo);
this.setState({
todos: todos
});
}
renderItems() {
var todos = this.state.todos;
todos.map((item) => {
<h4>{item}</h4>
});
}
render() {
return (
<div>
{this.renderItems()}
</div>
);
}
}
export default TodoList;
First time componentDidUpdate is called (which happens after first change in its props/state, which in your case happens after adding first todo) it adds this.props.addToList to this.state.todo and updates state. Updating state will run componentDidUpdate again and it adds the value of this.props.addToList to 'this.state.todo` again and it goes infinitely.
You can fix it with some dirty hacks but your approach is a bad approach overall. Right thing to do is to keep todos in parent component (Main), append the new todo to it in setNewTodo (you may probably rename it to addTodo) and pass the todos list from Main state to TodoList: <TodoList todos={this.state.todos}/> for example.
The basic idea of react is whenever you call setState function, react component get updated which causes the function componentDidUpdate to be called again when the component is updated.
Now problem here is you are calling setState function inside componentDidUpdate which causes the component to update again and this chain goes on forever. And every time componentDidUpdate is called it concat a value to the todo. So a time come when the memory gets full and it throws an error. You should not call setState function inside functions like componentWillUpdate,componentDidUpdate etc.
One solution can be to use componentWillReceiveProps instead of componentDidUpdate function like this:
componentDidUpdate(nextProps) {
var newTodo = nextProps.addToList;
this.setState(prevState => ({
todos: prevState.todos.concat(newTodo)
}));
}

React todo list displays todo onChange, not onSubmit

I'm a bit new to React, and with all new programming endeavors I am building a todo app. Everything seems to be working correctly except for one issue: When I enter a todo into the input field and click "submit", the todo is pushed into my array, however it doesn't immediately display. It is only when I change the text inside the input that the todo is displayed. I'm guessing this has something to do with the rendering happening on the handleChange function and not the handleSubmit function. Any help would be greatly appreciated.
Here is my AddTodo component
import React, { Component } from 'react';
import App from "./App"
import List from "./List"
class AddTodo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
array: []
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
event.preventDefault();
var array = this.state.array
array.push(this.state.value);
console.log(array)
}
render() {
return (
<div>
<form>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input onClick={this.handleSubmit} type="submit" value="Submit" />
</form>
<List array={this.state.array}/>
</div>
);
}
}
And my List component
import React, { Component } from 'react';
class List extends Component{
render(){
return(
<div>
{
this.props.array.map(function(item, index){
return <li key={index}>{item}</li>
})
}
</div>
)
}
}
export default List;
By default, invoking setState() calls the render() function.
More info here: ReactJS - Does render get called any time "setState" is called?
React renders an individual component whenever its props or state change. In order to make a change in the state, with a class component it's mandatory to use the this.setState() method, which among other things makes sure to call the render() when it's necessary.
Your handleSubmit() method is changing the array directly, which is forbidden (it's only allowed in the constructor in order to set the initial state)
If you use setState() it should work.

Is there a React lifecycle method to do something only when component receive props the first time?

I'm new to React so thank you for your patience in advance. Also using Redux.
I have a list of content pulled from the API, I display the text and a hidden text box and on a state change associated that alternates the visibility of the two. Essentially user can click on the text and edit the text, achieved by inverting the boolean and swapping the display. They can then save it and PUT to server etc.
Since my list length varies, I must initialize a number of state.isVisible[n]. equivalent to the number of content being displayed each time. This number must be counted, after the props come in. I am using Redux so the content is retrieved, stored, then given to props. It's done as the following:
constructor(props){
super(props);
this.state = {
isVisibleObj: {}
}
}
componentWillReceiveProps(){
const { isVisibleObj } = this.state
// set visibility of text box
let obj = {}
Object.keys(this.props.questions).forEach(key => obj[key] = false)
this.setState({isVisibleObj: obj})
}
My initial implementation was that in componentWillReceiveProps I do all the setState() to initialize the isVisible properties to a boolean.
The challenge I am having with this implementation is that, if a user open up multiple items for edit, and if she saves one of them, the PUT request on success would send back the edited content, now updating the store and props. This will trigger componentWillReceiveProps and reset all the visibilities, effectively closing all the other edits that are open.
Any suggestion on how to proceed?
I think you should make two components
List (NamesList.react)
import React, {PropTypes} from 'react';
import NameForm from './NameForm.react';
import Faker from 'Faker'
export default class NamesList extends React.Component {
constructor(){
super();
this.addItem = this.addItem.bind(this);
}
addItem(){
var randomName = Faker.name.findName();
this.props.addName(randomName);
}
render() {
let forms = this.props.names.map((name,i) => {
return <NameForm updateName={this.props.updateName} index={i} key={i} name={name} />
});
return (<div>
<div>{forms}</div>
<button onClick={this.addItem}>Add</button>
</div>);
}
}
NamesList.propTypes = {
names: PropTypes.arrayOf(PropTypes.string).isRequired
};
Form (NameForm.react)
import React, {PropTypes} from 'react';
export default class NameForm extends React.Component {
constructor(props) {
super(props);
this.updateName = this.updateName.bind(this);
this.state = {
showTextBox:false
}
}
updateName(){
this.setState({showTextBox:false});
this.props.updateName(this.props.index,this.refs.name.value);
}
render() {
if(this.state.showTextBox){
return (<div>
<input ref="name" defaultValue={this.props.name} />
<button onClick={this.updateName}>Save</button>
</div>);
}
return (<div onClick={() => {this.setState({showTextBox: !this.state.showTextBox})}}>
{this.props.name}
</div>);
}
}
NameForm.propTypes = {
name:PropTypes.string.isRequired
};
Invoke (App.js)
import React, { Component } from 'react';
import NamesList from './NamesList.react';
class App extends Component {
constructor(){
super();
this.addName = this.addName.bind(this);
this.updateName = this.updateName.bind(this);
this.state = {
names:['Praveen','Vartika']
}
}
addName(name){
let names = this.state.names.concat(name);
this.setState({
names: names
});
}
updateName(index,newName){
let names = this.state.names.map((name,i) => {
if(i==index){
return newName
}
return name;
});
this.setState({names:names});
}
render() {
return (
<NamesList names={this.state.names} updateName={this.updateName} addName={this.addName} />
);
}
}
export default App;
Now if your store changes after user saves something. React wont re-render Child component that didn't change

Categories

Resources