So I am attempting to learn react along with rails (Using rails purely as an API). Im making a simple to-do app and getting stuck when attempting to "Create" a list.
I have a "New List" component shown here, mostly taken from the react forms tutorial:
import React, { Component } from 'react';
import axios from 'axios';
class ListForm extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
description: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
this.setState({[e.target.name]: e.target.value});
}
handleSubmit(e) {
console.log("Form submitted with: " + this.state.value)
e.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Title:
<input name="title" type="text" value={this.state.value} onChange={this.handleInputChange} />
</label>
<label>
Description:
<textarea name="description" value={this.state.value} onChange={this.handleInputChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
export default ListForm;
I have my ListContainer shown here
import React, { Component } from 'react';
import axios from 'axios';
import List from './List';
import ListForm from './ListForm'
class ListContainer extends Component {
constructor(props){
super(props)
this.state = {
lists: []
}
this.addNewList = this.addNewList.bind(this)
}
componentDidMount() {
axios.get('/api/v1/lists.json')
.then(response => {
console.log(response)
this.setState({
lists: response.data
})
})
.catch(error => console.log(error))
}
addNewList(title, description) {
axios.post('/api/v1/lists.json', {
title: title,
description: description
})
.then(function (response) {
console.log(response);
const lists = [ ...this.state.lists, response.data ]
console.log(...this.state.lists)
this.setState({lists})
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div className="lists-container">
{this.state.lists.map( list => {
return (<List list={list} key={list.id} />)
})}
<ListForm onSubmit={this.addNewList} />
</div>
)
}
}
export default ListContainer;
My issue comes from a misunderstanding at the callback on submit. I understand that when I do an "onSubmit" on the form it's using addNewList function as a callback....but I really am not understanding how the connection from state in the ListForm is getting into that callback function. I obviously am doing something wrong because it does not work and currently console shows "Form submitted with: undefined" so it's not passing parameters correctly at all.
Im still pretty new to React and very rusty with JS (It's been a bit since i've used it so im sure there are some newbie mistakes here). Also axios is basically a "better" fetch fwiw.
I won't lie either, I don't exactly understand why we do this.handleSubmit = this.handleSubmit.bind(this); for example (along with the other similar ones)
You're so close! We just need to make a few adjustments.
First let's lose the bind statements, and just use arrow functions. Arrow functions have no this object, and so if you call on this inside that function, you will actually access the this object of your class instance. Neat.
Second, let's fix the typo on your handleChange function, so that your inputs are updating the component state properly.
Now, the real solution to your problem. You need to call the addNewList function in your parent component, ListContainer. How do we do that? Lets pass it down to the child component as a prop! You're almost there, but instead of using the keyword onSubmit={this.addNewList}, lets use something like handleSubmit instead. This is because onSubmit is actually a special keyword that will attach an event listener to the child component for submit, and we don't want that.
Now that your child component is taking in your function as a prop. We can call it inside the handleSubmit function. We then pass in the arguments, title and description. Now your child component is able to call the addNewList function in the parent component!
import React, { Component } from 'react';
import axios from 'axios';
import List from './List';
import ListForm from './ListForm'
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
lists: []
}
this.addNewList = this.addNewList.bind(this)
}
componentDidMount() {
axios.get('/api/v1/lists.json')
.then(response => {
console.log(response)
this.setState({
lists: response.data
})
})
.catch(error => console.log(error))
}
addNewList(title, description) {
axios.post('/api/v1/lists.json', {
title: title,
description: description
})
.then(function (response) {
console.log(response);
const lists = [...this.state.lists, response.data]
console.log(...this.state.lists)
this.setState({ lists })
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div className="lists-container">
{this.state.lists.map(list => {
return (<List list={list} key={list.id} />)
})}
<ListForm handleSubmit={this.addNewList} />
</div>
)
}
}
export default ListContainer;
import React, { Component } from 'react';
import axios from 'axios';
class ListForm extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
description: ''
};
}
handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
}
handleSubmit = (e) => {
this.props.handleSubmit(this.state.title, this.state.description);
e.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Title:
<input name="title" type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<label>
Description:
<textarea name="description" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
I think you have a typo as well. For the change event you have
<input name="title" type="text" value={this.state.value} onChange={this.handleInputChange} />
So the callback for change is this.handleInputChange. But in your code its called handleChange
But even if you've used the right naming, it will not work, because you need to bind that function as well.
That brings me to your question about this.handleSubmit = this.handleSubmit.bind(this);
The problem here is that when you pass a function as a callback it loses its context. Consider the following
const x = {
log: function() { console.log(this.val) },
val: 10
}
Now you can do
x.log(); // -> print 10
But when you do
y = x.log;
y(); // -> prints undefined
If you pass only the function around it looses its context. To fix this you can bind
x.log = x.log.bind(x);
y = x.log
y(); // -> prints 10
Hope this makes sense :)
Anyway, to come back to you question, you don't have to use bind, there is a better way
class ListForm extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
description: ''
};
}
handleChange = (e) => {
this.setState({[e.target.name]: e.target.value});
}
handleSubmit = (e) => {
console.log("Form submitted with: " + this.state.value)
e.preventDefault();
}
Although not tests, it might work right now!
Change value property of the inputs with this.state.title & this.state.description:
<input name="title" type="text" value={this.state.title} onChange={this.handleInputChange} />
<textarea name="description" value={this.state.description} onChange={this.handleInputChange} />
try printing info using
console.log("Form submitted with: ", this.state)
Regards to the .bind(this) :
When any event fires, an event object is assigned to callback function. So callback function or event handlers loses reference to class and this points to whichever event has been called.
Bind creates a new function that will have this set to the first parameter passed to bind()
Apart from .bind(this) arrow function takes care for this. but it's not preferable for long hierarchy.
Related
I created a simple date picker react js, after that I call API and get some data from API in the console, now I want to fetch API data on the web page.
Here is the code i used to call API function, I want map response data on a web page
import React, { Component } from 'react'
import axios from 'axios'
class PostForm extends Component {
constructor(props) {
super(props)
this.state = {
key: '',
}
console.log(this.state)
}
changeHandler = e => {
this.setState({ [e.target.name]: e.target.value })
}
submitHandler = e => {
e.preventDefault()
axios
.get(`http://127.0.0.1:8000/hvals_hash?key=${this.state.key}`)
.then(response => {
console.log(response.data)
})
.catch(error => {
console.log(error)
})
}
render() {
const { key } = this.state
return (
<center><div>
<form onSubmit={this.submitHandler}>
<div>
<h2> DATE PICKER</h2><br></br>
<input
type="text"
name="key"
value={key}
onChange={this.changeHandler}
/>
</div>
<br></br>
<button type="submit">Submit</button>
</form>
</div></center>
)
}
}
export default PostForm
Here is the explanation for this issue you can refer here,
import React, { Component } from 'react'
import axios from 'axios'
class PostForm extends Component {
constructor(props) {
super(props)
this.state = {
key: '',
// Where data will be saved.
data: [],
}
console.log(this.state)
}
changeHandler = e => {
this.setState({ [e.target.name]: e.target.value })
}
submitHandler = e => {
e.preventDefault()
axios
.get(`http://127.0.0.1:8000/hvals_hash?key=${this.state.key}`)
.then(response => {
// Updating the state to trigger a re-render
this.setState({data: response.data});
console.log(response.data)
})
.catch(error => {
console.log(error)
})
}
render() {
const { key } = this.state
return (
<center><div>
<form onSubmit={this.submitHandler}>
<div>
<h2> DATE PICKER</h2><br></br>
<input
type="text"
name="key"
value={key}
onChange={this.changeHandler}
/>
</div>
<br></br>
<button type="submit">Submit</button>
</form>
<div>
{this.state.data.map((dataObjectToDisplay) => {
return (
<div>
{
<ol>{this.state.data}</ol>
}
</div>
);
})}
</div>
</div></center>
)
}
}
export default PostForm
add a state (add an object property to this.state) and inside the submit handler, after getting the response in Axios, use setState({[name of state]: response.data }).
That takes care of updating the state. As for where to display them, it is up to you.
You can copy the code anywhere inside the render method. If u want to display it under the form, paste it inside the div container after the form.
For better controlling of the state, you can check one of the state-management tools like redux or simply use global context and implement routing using react-router –
Credits to #MrCeRaYA
First you have to save the data somewhere inside the app. Create a state and use setState to update it. Now that you have the latest data, you can use JSX's magic to display them using map.
Example:
<div>
{this.state.data.map((dataObjectToDisplay) => {
return (
<div>
{/* Include your data in a way you want here, you can also replace the div with any other element (li) */}
</div>
);
})}
</div>
<div>
{this.state.data.map((dataObjectToDisplay) => {
return (
<div>
{/* Include your data in a way you want here, you can also replace the div with any other element (li) */}
</div>
);
})}
</div>
I'm using a formatter.js to format some input box. I want to connect this formatter to my react app so I've write a simple module but onChange function doesn't fire. When I remove the formatter library the input box works as planned. But I want to format the inputs and also use the values inside my React application.
After a brief search over the internet I've came across with this question;React: trigger onChange if input value is changing by state? but I'm not sure how can I apply that solution to my application.
ReactMaskedInput.js
import React, { Component } from 'react'
class ReactMaskedInput extends Component {
constructor(props) {
super(props)
this.onChangeHandler = this.onChangeHandler.bind(this)
this.state = {
value: ""
}
}
onChangeHandler(event) {
this.setState({
value: event.target.value
})
alert(this.state.value)
}
componentDidMount() {
let dataMask = this.props.dataMask
window.$(`#${this.props.id}`).formatter({
pattern: dataMask
});
}
render() {
return (
<div >
<h3>
<b>{this.props.header}</b>
</h3>
<input
id={this.props.id}
type="text"
placeholder={this.props.placeHolder}
className="form-control"
onChange={event => this.onChangeHandler(event)}
name={this.props.name}
>
</input>
</div>
)
}
}
export default ReactMaskedInput
Index.js
ReactDOM.render(<ReactMaskedInput
id="myMaskedInput"
header="Masked Input Phone"
onChange={(event) => { deneme(event); }}
dataMask={"({{999}}) {{999}}-{{9999}}"}
name="maskedInput1"
placeHolder="Please enter a valid phone number"
validationInitiatorNr={10}
// onChange={(phoneNr) => validatePhone(phoneNr)}
/>, document.getElementById('myFormattedInput'))
Fix your onChangeHandler code
You have to call the 'onChange' handler you passed as an attribute in code of your ReactMaskedInput class explicitly. I guess you are assuming that it would get called automatically. Note that ReactMaskedInput is a component you created, and not an HTML tag 'onChange' of which gets called by React.
onChangeHandler(event) {
this.setState(() => ({
value: event.target.value
}), () => {
this.props.onChange(event) // Call 'onChange' here
alert(this.state.value) // alert should be inside setState callback
})
}
I was trying to DRY up my react forms a bit, so I wanted to move my basic input-handling function to a utility module and try to reuse it. The idea was to update an object that represented the state, return it in a Promise, and then update the state locally in a quick one-liner.
Component
import handleInputChange from "./utility"
class MyInputComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: {
title: ""
}
};
}
render() {
return (
<input
type="text"
id="title"
value={this.state.data.title}
onChange={e => handleInputChange(e, this.state).then(res => {
this.setState(res);
})}
/>
)
}
};
utility.js
export const handleInputChange = (event, state) => {
const { id, value } = event.target;
return Promise.resolve({
data: {
...state.data,
[id]: value
}
});
};
It seems to work fine, however the issue is that the input's cursor always jumps to the end of the input.
If I use a normal local input handler and disregard being DRY, then it works fine.
For example, this works without issue regarding the cursor:
Component
class MyInputComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: {
title: ""
}
};
}
handleInputChange = event => {
const { id, value } = event.target;
this.setState({
data: {
...this.state.data,
[id]: value
}
});
};
render() {
return (
<input
type="text"
id="title"
value={this.state.data.title}
onChange={this.handleInputChange}
/>
)
}
};
Any idea why the cursor issue would happen when I tried to be DRY? Is the promise delaying the render, hence the cursor doesn't know where to go? Thanks for any help.
I'm a little confused with what you are trying to do in the long run. If you want to be DRY maybe your react component could look like this
render() {
return (
<input
type={this.props.type}
id={this.props.title}
value={this.props.value}
onChange={this.props.handleInputChange}
/>
)
}
this way you pass everything in and it stay stateless and everything is handle at a high component
however doing the way you have asked could you not use something like the below and you would not need to use a promise to return an object?
onChange={e => this.setState(handleInputChange(e, this.state))}
Hy!
I am having an issue with my react code. My task is to call from iTunes API which i do with fetch then I process the data. But I cannot save it as a variable to be able to pass it around later.
import React, { Component } from 'react';
class SearchField extends Component{
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (event) => {
this.setState({value: event.target.value});
}
handleSubmit = (event) => {
event.preventDefault();
fetch(`https://itunes.apple.com/search?media=music&term=${this.state.value.toLowerCase()}`)
.then((resp) => resp.json())
.then(searchRes => searchRes.results[0].artistName)
.catch(err => console.log(err));
}
render() {
return(
<section className="hero is-primary">
<div className="hero-body">
<div className="container">
<form onSubmit={this.handleSubmit}>
<input className="input is-primary" type="text" value={this.state.value} onChange={this.handleChange} placeholder="Search for artist" />
<input className="button is-info" type="submit" value="Search" />
</form>
</div>
</div>
</section>
)
}
}
export default SearchField;
I'd have to use the fetched data later, i just need the artist name first.
If I log the value (searchRes.results[0].artistName, i get the correct value, but if i want to save it for later use i only got empty console.log back.
I've tried several approaches but I never get my result back.
Help me out please.
Remember that data flow in React is unidirectional. If you want to share the data around your app the search component should not be the component that fetches the data. That should be left to a parent component (maybe App). That component should have a function that handles the fetch request, and you can then pass a reference to that function down to the search component to call when the button is clicked. Then, once that data is loaded, the parent (App) component can pass all the relevant data down to the child components.
Here's a quick mock-up based on your existing code:
class Search extends {
constructor(props) {
super(props);
this.state = { url: '' };
this.handleKey = this.handleKey.bind(this);
}
handleKey(e) {
const url = e.target.value;
this.setState({ url });
}
render() {
const { url } = this.state;
// grab the function passed down from App
const { fetchData } = this.props;
return (
<input onKeyUp={this.handleKey} value={url} />
// Call that function with the url when the button is clicked
<button onClick={() => fetchData(url)}>Click</button>
)
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = { data: [] };
this.fetchData = this.fetchData.bind(this);
}
// App contains the fetch method
fetchData(url) {
fetch(url)
.then(res => res.json())
// Update the App state with the new data
.then(data => this.setState({ data });
}
render() {
const { data } = this.state;
// Sanity check - if the state is still empty of data, present
// a loading icon or something
if (!data.length) return <Spinner />
// otherwise return the rest of the app components
// passing in the fetch method as a prop for the search component
return (
<OtherComponent data={data} />
<Search fetchData={this.fetchData} />
)
}
}
Please specify what you mean by
but if i want to save it for later use i only got empty console.log back
I think the correct way to handle your problem is by passing a callback function to your component's props that gets called whenever you press search and a search result is found, like this: https://codesandbox.io/s/xpq171n1vz
Edit: Note that while this answer has been accepted and is a way to solve your problem, Andy's answer contains solid and elaborate advice on how to actually structure your components.
I have a component where I am making a second setState() as a callback in the first setState(). Is this poor practice? Is there another way to call two setStates synchronously?
Initially when I called updateData() in the first setState() there was a delay in rendering the correct data in the myComponent component. It was one 'step' behind. This works, but is it conventional?
import React, { Component } from "react";
import MyComponent from "../../components/MyComponent";
import RaisedButton from "material-ui/RaisedButton";
import { generateData } from "./generateData";
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
data: []
};
}
updateData(){
this.setState({
data: generateData(this.state.text)
})
}
handleChange(e) {
this.setState({
text: e.target.value
}, () => {
this.updateData(this.state.text)
});
}
handleSubmit(e) {
e.preventDefault();
}
render() {
return (
<div>
<h2>Input</h2>
<form onSubmit={e => this.handleSubmit(e)}>
<textarea
value={this.state.text}
onChange={e => this.handleChange(e)}
/>
<div>
<RaisedButton type="submit"/>
</div>
</form>
<h2>Output</h2>
<MyComponent data={this.state.data} />
</div>
);
}
}
export default App;
The problem seems to be that you are updating data from this.state.text. Instead you can update both text and data in a single call by referencing the original input value:
handleChange(e) {
this.setState({
text: e.target.value,
data: generateData(e.target.value),
});
}
This is certainly preferred over making two calls to setState (which implies potentially rerender the component twice).