DRY react input handling causes cursor to jump - javascript

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))}

Related

How can I trigger a method in a subcomponent when parent changes props?

I have a React web application that has three components, a parent and two child subcomponents. I've left out the JavaScript related to <HeaderComponent /> because it isn't relevant to this question.
class App extends Component {
token = null;
constructor(props) {
super(props);
this.state = {
AllInvoices: [],
CurrentInvoice: null
};
this.setCurrentInvoice = this.setCurrentInvoice.bind(this);
}
setCurrentInvoice = (sharedValue) => {
this.setState({
CurrentInvoice: sharedValue
});
}
componentDidMount()
{
fetch("http://api.app.local/api/getALLinvoices", {
"method": "GET"
})
.then(resp => {
this.setState({
AllInvoices: resp.Result
CurrentInvoice: resp.Result[0]
});
});
}
componentDidUpdate()
{
// this DOES trigger
console.log("App componentDidUpdate triggered");
}
render() {
return (
<>
<HeaderComponent AllInvoices={this.state.AllInvoices} setCurrentInvoice={this.setCurrentInvoice} />
<MainFormComponent CurrentInvoice={this.state.CurrentInvoice} />
</>
)
}
}
class MainFormComponent extends Component {
token = null;
constructor(props) {
super(props);
this.state = {
Field1Value: "",
Field2Value: "",
Field3Value: "",
// ...
Field12Value: ""
};
}
componentDidUpdate()
{
//> For some reason this does NOT trigger when CurrentInvoice is updated
console.log("MainFormComponent componentDidUpdate triggered");
}
getInvoiceDetailsAndUpdateForm = () =>
{
fetch("http://api.app.local/api/getinvoicedetails", {
"method": "POST",
"body": {
"invoice_id": this.props.CurrentInvoice.Id
}
})
.then(resp => {
/*
* Run various business logic and update 12+ form fields with AJAX response
*
*/
});
}
render() {
return (
<>
<TextField Value={this.state.Field1Value} />
<TextField Value={this.state.Field2Value} />
<TextField Value={this.state.Field2Value} />
{ /* ... */ }
<TextField Value={this.state.Field12Value} />
</>
)
}
}
class HeaderComponent extends Component {
token = null;
constructor(props) {
super(props);
this.state = {
MiscVariable: ""
};
}
setUpdateCurrentInvoice = (row) =>
{
this.props.setCurrentInvoice(row);
}
render() {
return (
<>
{this.props.AllInvoices.map((row, i) => (
<Button onClick={() => { this.setUpdateCurrentInvoice(row); }} />
))}
</>
)
}
}
Within the App component an AJAX call returns all invoices, and sets an initial value for this.state.CurrentInvoice. Afterwards, buttons in <HeaderComponent /> or <MainFormComponent /> can change CurrentInvoice.
When CurrentInvoice within App is changed, I want to trigger getInvoiceDetailsAndUpdateForm within the <MainFormComponent /> so that I can perform another AJAX call and run other business logic within that component.
What I'm finding is that within <MainFormComponent /> I can't seem to be able to "subscribe" to props.CurrentInvoice value changes. Within the render() method in that Component I see the change. But, that hasn't helped me because I want to trigger getInvoiceDetailsAndUpdateForm.
Does anyone have any ideas on how I can achieve the outcome I want?
Update:
The original code I published did not contain code related to the HeaderComponent. I've now included that to help draw the whole picture.
Basically what's happening is that the user clicks on a button in HeaderComponent which then calls setCurrentInvoice. When this happens componentDidUpdate is triggered, but only in the parent component. I am trying to figure out why componentDidUpdate within MainFormComponent is NOT also firing.
What you are looking for is componentDidUpdate. You can compare prevProps and currentProps and perform necessary actions
you would want to implement this lifecycle method in your MainFormComponent
Also, here's this example which is emulating the behavior you are having.
if you see App is the component storing the state and the handler,
and ChildComponent has the componentDidUpdate in it
now when you click either in App or AnotherChildComponent
it fires the state update, since ChildComponent has ComponentDidUpdate and it is receiving the counter prop you can clearly see it printing the previous and current values.
One thing to note - componentDidUpdate does not get immediately fired but when the state/prop changes so you may not see it fired on the first render but as soon as your api returns something and updates the state you will see it's getting fired for the 1st time in your MainForm component
Also, if you can post some proof of it not firing like screenshot of console.log would be helpful.
I don't see a point of componentDidUpdate not firing.

update parent's state fom child component using onChange

I am trying to update the number of event from the parent component by using an input form from the child component, but there is something I am not seeing it either doesn't work or shows undefined
class App extends Component {
state = {
numberOfEvents: 32,
};
.....
updateNumberOfEvents = (eventNumber) => {
this.setState({ numberOfEvents: eventNumber });
};
render() {
return (
<div className="App">
<NumberOfEvents updateNumberOfEvents={this.updateNumberOfEvents} />
}
</div>
class NumberOfEvents extends Component {
state = {
numberOfEvents: 32,
};
handleInputChanged = (event) => {
const value = event.target.value;
this.setState({
numberOfEvents: value,
});
this.props.updateNumberOfEvents(value);
};
render() {
const numberOfEvents = this.state.numberOfEvents;
return (
<div className="numberOfEvents">
<form>
<label for="fname"> Number of Events:</label>
<input
type="text"
className="EventsNumber"
value={numberOfEvents}
onChange={this.handleInputChanged}
/>
</form>
</div>
);
}
}
export default NumberOfEvents;
this.setState({
numberOfEvents: value,
}, () => {
this.props.updateNumberOfEvents(value);
}
);
The details are here.
While this answer does make it work and correctly highlights that setState calls are asynchronous, I would suggest removing the local state inside NumberOfEvents entirely, as you currently have multiple sources of truth for your form.
Update your onChange handler:
handleInputChanged = (event) => {
this.props.updateNumberOfEvents(event.target.value);
};
and pass down the value from the parent:
<NumberOfEvents
updateNumberOfEvents={this.updateNumberOfEvents}
numberOfEvents={this.state.numberOfEvents}
/>
and use that value inside your child component:
<input
type="text"
className="EventsNumber"
value={this.props.numberOfEvents}
onChange={this.handleInputChanged}
/>
Having one source of truth is less error prone and easier to maintain, as illustrated by your current bug.
I agree with hotpink but i tried your code on codesandbox...it does not show the value undfined. Here is the link https://codesandbox.io/s/stackoverflow-iow19?file=/src/App.js.
Check console

React - state doesn't update in class component even though the message printed on the screen changes

I have an App component which holds an input. Every time I type in the input, the value of the input updates and a Message component prints a different message, depending on how long the input is. At the same time, a third component called Character print to the screen every letter of the string, individually. The desired behavior is that when I click on one of the letters, it gets removed from the string, the new string is displayed on the screen and the input also gets updated with the new string.
I used some console.logs to debug and everything seems to be happening as expected, until the last step when I am trying to update the state, but for some reason, it doesn't get updated.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { text: "" };
}
render() {
const handleUpdateText = event => {
this.setState({
text: event.target.value
});
};
const inputLength = this.state.text.length;
const toArray = this.state.text.split("");
const handleDeleteLetter = index => {
toArray.splice(index, 1);
console.log(toArray);
const updatedArray = toArray.join("");
console.log(updatedArray);
this.setState({ text: updatedArray });
console.log(this.state.text);
};
return (
<>
<input type="text" onChange={handleUpdateText} />
<Message inputLength={inputLength} />
{toArray.map((letter, index) => (
<Character
key={index}
theLetter={letter}
deleteLetter={() => handleDeleteLetter(index)}
/>
))}
</>
);
}
}
class Message extends React.Component {
render() {
const { inputLength } = this.props;
let codeToPrint = "The text is long enough!";
if (inputLength <= 5) {
codeToPrint = "The text is not long enough!";
}
return <p>{codeToPrint}</p>;
}
}
class Character extends React.Component {
render() {
const { theLetter, deleteLetter } = this.props;
return (
<div
style={{
display: "inline-block",
padding: "16px",
textAlign: "center",
margin: "16px",
backgroundColor: "tomato"
}}
onClick={deleteLetter}
>
{theLetter}
</div>
);
}
}
The complete code is here:
https://codesandbox.io/s/react-the-complete-guide-assignment-2-list-conditionals-e6ty6?file=/src/App.js:51-1007
I don't really understand what am I doing wrong and I have a feeling is somehow related to a life cycle method. Any answer could help. Thank you.
State is getting updated, you just need to pass value prop to the input so that input's value can be in sync with your state
<input type="text" value={this.state.text} onChange={handleUpdateText} />
And you're not seeing updated state just after setting it because setState is asynchronous. That's why the console statement just after the setState statement shows the previous value.
Also you should move functions out of your render method, because everytime your component re-renders, new functions would be created. You can declare them as class properties and pass their reference
handleUpdateText = event => {
this.setState({
text: event.target.value
});
};
render() {
.......
return (
<>
<input type="text" onChange={this.handleUpdateText} />

Confusion on React Callback's on Forms

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.

React passing fetched data to another component

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.

Categories

Resources