I'm trying to build an example CRUD app with React and React Router, and I can't figure out why state isn't passing into a child component the way I'm expecting it to. When I hit the edit route, it renders the Edit component, which grabs the kitten I want from the database and sends it's info to a Form component which is used both for editing an existing kitten or adding a new one.
Here's the Edit component:
import React, { Component } from 'react';
import axios from 'axios';
import { match } from 'react-router-dom';
import Form from './Form';
export default class Edit extends Component {
constructor(props) {
super(props)
this.state = {}
}
componentDidMount() {
axios.get(`/updateKitten/${this.props.match.params.id}`)
.then(res => {
const kitten = res.data
this.setState({ kitten })
console.log(this.state.kitten.name) //Sammy, or something
})
.catch(err => console.log(err))
}
render() {
return (
<Form
name={this.state.kitten.name} // throws error or undefined
description={this.state.kitten.description} //throws error or undefined
route={this.props.match.params.id}
/>
)
}
}
The Edit component passes name, description, and route to this Form component:
import React, { Component } from 'react';
import axios from 'axios';
export default class Add extends Component {
constructor(props) {
super(props)
this.state = { name: this.props.name, description: this.props.description}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
const name = e.target.name;
const value = e.target.value;
this.setState({
[name]: value
});
}
handleSubmit(e) {
axios.post(`/addKitten/${this.props.route}`, this.state)
.then(this.setState({ name: '', description: '' }))
e.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>Name</label>
<input type='text' name="name" value={this.state.name}
onChange={this.handleChange}/>
<label>Description</label>
<input type='text' name="description" value={this.state.description}
onChange={this.handleChange}/>
<input type='submit' value='Submit' />
</form>
)
}
}
And I get the following error:
bundle.js:28950 Uncaught TypeError: Cannot read property 'name' of undefined
from trying to send that info as props to the Form component.
What am I doing wrong?
Two things,
First: In your edit component, you have not initialised kitten state and you are setting it based on the API result. However, componentDidMount is called after the component has been called and hence the DOM has been rendered once and the first time it did not find any value as this.state.kitten.name or this.state.kitten.description .
Its only after the API is a success that you set the kitten state. Hence just make a check while rendering.
Second: You have console.log(this.state.kitten.name) after the setState function. However setState is asynchronous. See this question:
Change state on click react js
and hence you need to specify console.log in setState callback
You code will look like
export default class Edit extends Component {
constructor(props) {
super(props)
this.state = {}
}
componentDidMount() {
axios.get(`/updateKitten/${this.props.match.params.id}`)
.then(res => {
const kitten = res.data
this.setState({ kitten }. function() {
console.log(this.state.kitten.name) //Sammy, or something
})
})
.catch(err => console.log(err))
}
render() {
return (
<Form
name={this.state.kitten.name || '' }
description={this.state.kitten.description || ''}
route={this.props.match.params.id}
/>
)
}
}
When your Edit component loads for the first time, it doesn't have a kitten. Hence the error.
What you need to do is the create an empty kitten. In your Edit component...
this.state = { kitten: {name:''} }
This will set the kitten's name to an empty string on the very first mount/render of your component.
Related
I am having a question about how to implement a callback function. In my case, I have a React app with this structure: App > Child > Button components
The problem is I do not know how to write a callback function from Button to Child
I would like to update a value in Child (e.g: inputFromButton) after clicking the button in Button Component. The handleClick() is triggered and a value will be sent to the Child component.
Could someone help me to do this?
Here is my code:https://codesandbox.io/s/nifty-stonebraker-0950w8
The App component
import React from 'react';
import Child from './Child';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: 'Data from App'
}
}
handleCallback = (childData) => {
this.setState({ data: childData })
}
render() {
const { data } = this.state;
return (
<div>
<Child dataFromApp={data} />
</div>
)
}
}
export default App
The Child component
import React from 'react';
import { renderButton } from './Button';
class Child extends React.Component {
state = {
inputFromApp: "",
inputFromButton: ""
}
componentDidMount() {
this.setState({
inputFromApp: this.props.dataFromApp
})
}
render() {
const renderButtonItem = renderButton(this.props);
const inputFromApp = this.state.inputFromApp
const inputFromButton= this.state.inputFromButton
return (
<div>
<input value={inputFromApp}></input>
<br></br>
<input value={inputFromButton}></input>
<div>{renderButtonItem}</div>
</div>
)
}
}
export default Child
The Button component
import React from 'react';
export const renderButton = (props) => {
const handleClick = () => {
console.log('handleClick() props data from App: ' + props.dataFromApp)
}
return (
<button onClick={handleClick}>Click</button>
)
}
renderButton is a function component and, therefore, needs to be in PascalCase: RenderButton (although it would be better off as Button).
Move handleClick to the Child component.
Then in Button the call to handleClick should be props.handleClick since handleClick will now be a property of the props object passed into the component. We don't need to pass down the data as a prop to the button but can, instead just log the data prop passed into Child.
handleClick = () => {
console.log(`handleClick(): ${props.dataFromApp}`);
}
In Child, instead of calling renderButton, import Button, and then use that in the render passing down the handler in the props. By doing this you're making the component as "dumb" as possible so it can be reused elsewhere in the application.
<Button handleClick={this.handleClick} />
I am facing issues while updating the values. Initially I am taking the values from the parent class to put into the text box, and then if I want to update the values into the form through the child component it should basically set the state in child component and pass the updated values to the API. But now when I try to change the values in the text box, it only changes one character and doesn't keep track of the state of all the props. How can I solve this? I have tried using the defaultValue it does change the values but it cannot keep track of the state change.
PS: The updateToApi is just a sample function that is using post to update values into the api
my sample project is here
https://codesandbox.io/s/sad-perlman-ukb68?file=/src/parent.js
#class Parent#
import React from "react";
import "./styles.css";
import Child from "./child";
class Parent extends React.Component {
constructor() {
super();
this.state = {
data: {
username: ["mar"],
name: [null]
}
};
}
updateToApi(data) {
var username: data.username;
var name: data.name;
}
render() {
return (
<Child data={this.state.data} updateToApi={this.updateToApi.bind(this)} />
);
}
}
export default Parent;
##class Child##
import React from "react";
import "./styles.css";
import { Button } from "react-bootstrap";
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
username: "",
name: ""
};
}
handleSubmit = e => {
e.preventDefault();
};
handleChange = e => {
const data = { ...this.state };
data[e.currentTarget.name] = e.currentTarget.value;
this.setState({ data });
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input
type="text"
name="username"
value={
this.props.data.username !== "undefined"
? this.props.data.username
: this.state.username
}
onChange={this.handleChange}
/>
</label>
<b />
<label>
Name:
<input
type="text"
name="Name"
value={
this.props.data.name !== "undefined"
? this.props.data.name
: this.state.name
}
onChange={this.handleChange}
/>
</label>
<br />
<Button variant="primary" onClick={this.props.updateToApi} />
</form>
</>
);
}
}
export default Child;
Why do you have 2 separate states? You should get rid of the state in your Child component entirely and only work with the Parent's state. Put HandleChange function in your Parent component also and pass it down through props.
UPD
Well, if you want for changes in your inputs to be visible, you could change the onchange handler in your Child coponent to
handleChange = e => {
this.setState({
[e.currentTarget.name] : e.currentTarget.value });
};
and the Input value just to this.state.username
Though i'm still having hard time to grasp what you are trying to accomplish here. Having 2 separate conditional states for the input fields is just too complicated. Imagine if your app would be a bit more complex? You'd lost yourself to debugging this stuff:
value={ this.props.data.username !== "undefined"
? this.state.username
: this.state.username
}
So here i highly recommend you to reevaluate all your data strucuture and data flow within the app. You should have the least amount of sources of truth within your app. Ideally one. So just use the main state in the Parent component and pass down the props that are required.
I'm passing a recently updated state variable to a child component. In that child component I'm able to console.log(this.props.response.message) and see the expected result of the recently updated variable. However, the this.props.response.message is not rendering. Do I need to perform some action in a React Lifecycle method? If so, which one and how would that look?
Parent Component
import React, { Component } from 'react';
import axios from 'axios';
import FormSuccess from './FormSuccess';
export default class NewRuleForm extends Component {
constructor(props) {
super();
this.state = {
submitSuccess: false,
isOpen: false,
response: {}
};
this.handleSubmit = this.handleSubmit.bind(this);
}
async handleSubmit(e) {
let response = await axios.post('localhost:5000',
{
data: e.target.id.value
}
)
if (response.data.success) {
this.setState({
submitSuccess: true,
response: response.data
})
} else {
this.setState({
submitSuccess: false,
})
}
return response
}
}
render() {
if (this.state.submitSuccess === true) {
return <FormSuccess result={this.state.response} />
}
return (
<form onSubmit={this.handleSubmit}>
<input type="text" name="id"/>
</form>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Child Component
import React, { Component } from 'react';
export default class FormSuccess extends Component {
constructor(props) {
super();
}
render() {
console.log(this.props.result.message) // works
return (
<div>
<h1>Successful Form Submission</h1>
{/* below doesn't render */}
<p>{this.props.result.message}</p>
</div>
);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Again, in that child component I am able to console.log(this.props.response.message) and see the expected result of the recently updated variable. However, it does not render.
Also you should be careful about data type. If data type is json or something like that, you cant render it but you can write on console.
You are passing data in result prop,
<FormSuccess result={this.state.response} />
You should use it like this,
{this.props.result} // Normal case
If you are getting object in result and want to retrieve message from it then use this,
{this.props.result.message}
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