Making second setState in callback - bad practice? - javascript

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

Related

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.

Passing handleSubmit() to child component does not modify parent's state

I am new to React and Javascript.
I am trying to have a user fill in a form that describes what a "Mob" should look like. When the user hits submit, I expect handleSubmit() (passed in through a parent) to modify the parent's state, which is an object. However, this behavior is not happening.
Here is the parent component, called App.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mob: new Mob("", "")
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
alert("A name was submitted: " + this.state.vnum + " event value: " + event.state.vnum);
const newMob = new Mob(event.state.vnum, event.state.shortDesc);
this.setState({
mob: newMob
});
}
render() {
return (
<div>
<MobForm mob={this.state.mob} onSubmit={() => this.handleSubmit} />
{console.log("parsed mob vnum: " + this.state.mob.vnum)}
</div>
);
}
}
The child component, called MobForm
class MobForm extends React.Component {
render() {
return (
<div>
<form onSubmit={this.props.onSubmit}>
<CreateStringInputField
name="vnum"
label="vnum:"
/>
<CreateStringInputField
name="shortDesc"
label="Short Desc:"
/>
<input type="submit" value="Submit" />
</form>
{console.log(this.state)}
</div>
);
}
}
Which is calling CreateStringInputField()
function CreateStringInputField(props) {
return (
<div name="row">
<label>
<b>{props.label}</b>
<br />
<input
type="text"
name={props.name}
label={props.label}
/>
</label>
</div>
);
}
And, in case it matters, here is what "Mob" looks like.
class Mob {
constructor(vnum, shortDesc) {
this.vnum = vnum;
this.shortDesc = shortDesc;
};
}
I expect to see {console.log("parsed mob vnum: " + this.state.mob.vnum)} print out the vnum as entered by a user. Instead, I see nothing. How can I achieve this expected output?
With React you won't need to work with plain classes. Instead, the class extends a provided React component (Component or PureComponent) or if you don't need state, then'll use plain functions that just return some JSX.
Working example: https://codesandbox.io/s/simple-form-kdh3w
index.js
import React from "react";
import { render } from "react-dom";
import MobForm from "./components/MobForm";
// simple function that returns "MobForm" and it gets rendered by ReactDOM
function App() {
return <MobForm />;
}
// applies "App" to a <div id="root"></div> in the public/index.html file
render(<App />, document.getElementById("root"));
components/MobForm/index.js (stateful parent component)
import React, { Component } from "react";
import Form from "../Form";
const initialState = {
vnum: "",
shortDesc: ""
};
// a stateful parent that manages child state
class MobForm extends Component {
constructor(props) {
super(props);
this.state = initialState;
// since the class fields are normal functions, they'll lose context
// of "this" when called as a callback. therefore, they'll need
// to be bound to "this" -- via bind, "this" is now referring to
// the Class, instead of the global window's "this")
this.handleChange = this.handleChange.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// a reusable class field that stores an input's value via its "name"
// for example: [vnum]: "12345", [shortDesc]: "A number"
// using object destructuring for shorter syntax:
// [event.target.name]: event.target.value
handleChange({ target: { name, value } }) {
this.setState({ [name]: value });
}
// a class field to reset state
handleReset() {
this.setState(initialState);
}
// a class field to "submit" the form and alert what's currently in state
handleSubmit(event) {
// preventDefault prevents page refreshes
event.preventDefault();
// JSON.stringify allows you to print the contents of an object
// otherwise, you'll just see [object Object]
alert(JSON.stringify(this.state, null, 4));
// clears state after submitting form
this.handleReset();
}
render() {
return (
// passing down state via the spread operator, shorthand for
// "vnum={this.state.vum}" and "shortDesc={this.state.shortDesc}",
// as well as, passing down the class fields from above
<Form
{...this.state}
handleChange={this.handleChange}
handleReset={this.handleReset}
handleSubmit={this.handleSubmit}
/>
);
}
}
export default MobForm;
components/Form/index.js (a child function that returns some form JSX)
import React from "react";
import PropTypes from "prop-types";
import Input from "../Input";
// using object destructuring to pull out the MobForm's passed down
// state and fields. shorthand for using one parameter named "props"
// and using dot notation: "props.handleChange", "props.handleReset", etc
function Form({ handleChange, handleReset, handleSubmit, shortDesc, vnum }) {
return (
<form style={{ width: 200, margin: "0 auto" }} onSubmit={handleSubmit}>
<Input name="vnum" label="vnum:" value={vnum} onChange={handleChange} />
<Input
name="shortDesc"
label="Short Desc:"
value={shortDesc}
onChange={handleChange}
/>
<button type="button" onClick={handleReset}>
Reset
</button>{" "}
<button type="submit">Submit</button>
</form>
);
}
// utilizing "PropTypes" to ensure that passed down props match
// the definitions below
Form.propTypes = {
handleChange: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
shortDesc: PropTypes.string,
vnum: PropTypes.string
};
export default Form;
components/Input/index.js (a reuseable input function)
import React from "react";
import PropTypes from "prop-types";
// once again, using object destructuring to pull out the Form's
// passed down state and class fields.
function Input({ label, name, value, onChange }) {
return (
<div name="row">
<label>
<b>{label}</b>
<br />
<input
type="text"
name={name}
label={label}
value={value}
onChange={onChange}
/>
</label>
</div>
);
}
// utilizing "PropTypes" to ensure that passed down props match
// the definitions below
Input.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
onChange: PropTypes.func.isRequired
};
export default Input;
In this line
<MobForm mob={this.state.mob} onSubmit={() => this.handleSubmit} />
you are defining an anonymous function that returns your handleSubmit function.
In your form
<form onSubmit={this.props.onSubmit}>
onSubmit will execute the this.props.onSubmit which just returns the handleSubmit function but it wont execute it. To fix it just change MobForm to pass handleSubmit directly instead of passing it in an anonymous function:
<MobForm mob={this.state.mob} onSubmit={this.handleSubmit} />
To handle the submission correctly you need to convert your form inputs to managed components. See docs here
Something like this would be a good start:
class MobForm extends React.Component {
constructor(props) {
super(props);
this.state = {
vnum: '',
shortDesc: '',
};
this.handleChangeVnum = this.handleChangeVnum.bind(this);
this.handleChangeShortDesc = this.handleChangeShortDesc.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChangeVnum(event) {
this.setState({vnum: event.target.value});
}
handleChangeShortDesc(event) {
this.setState({shortDesc: event.target.value});
}
handleSubmit(event) {
this.props.onSubmit(this.state);
event.preventDefault();
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<CreateStringInputField
name="vnum"
label="vnum:"
value={this.state.vnum}
onChange={this.handleChangeVnum}
/>
<CreateStringInputField
name="shortDesc"
label="Short Desc:"
value={this.state.shortDesc}
onChange={this.handleChangeShortDesc}
/>
<input type="submit" value="Submit" />
</form>
{console.log(this.state)}
</div>
);
}
}
And update CreateStringInputField()
function CreateStringInputField(props) {
return (
<div name="row">
<label>
<b>{props.label}</b>
<br />
<input
type="text"
name={props.name}
label={props.label}
value={props.value}
onChange={props.onChange}
/>
</label>
</div>
);
}
I was able to get my desired behavior by passing a function to MobForm which updates this.state.mob.
App
class App extends React.Component {
state = {
mob: new Mob("", "")
};
updateMob = newMob => {
this.setState({
mob: newMob
});
};
render() {
return (
<div>
<MobForm mob={this.state.mob} onSubmit={this.updateMob} />
</div>
);
}
}
I then made MobForm maintain vnum, shortDesc state that I could use in my onChange()
MobForm
state = { vnum: "", shortDesc: "" };
handleSubmit = event => {
event.preventDefault();
const mob = new Mob(this.state.vnum, this.state.shortDesc);
this.props.onSubmit(mob);
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<CreateStringInputField
name="vnum"
value={this.state.vnum}
onChange={event => this.setState({ vnum: event.target.value })}
/>
<CreateStringInputField
name="short desc"
value={this.state.shortDesc}
onChange={event => this.setState({ shortDesc: event.target.value })}
/>
<input type="submit" value="Submit" />
</form>
</div>
);
}
}

reactjs - communication between two components

I want to call a function from one component to another. I've tried a few different things after googling, but as I'm new to reactjs I can't seem to work it out. I thought I could import the state and then change it from the other component (see below) but I think I'm barking up the wrong tree?
I want to call showMenu() in header.js from locationform.js
header.js
import React, { Component, PropTypes } from 'react';
import ReactF1 from 'react-f1';
// Styles.
import styles from './Header.css';
// Elements.
import Menu from './elements/Menu';
// Menu states.
import { states, SHOWBUTTON, IDLE, HIDE, OUT } from './elements/Menu/states';
import transitions from './elements/Menu/transitions';
function changeOffset() {
document.querySelector('svg path').style.strokeDashoffset = 0;
setTimeout("document.querySelector('svg path').style.strokeDashoffset = 171", 2000);
}
export default class Header extends Component {
static get propTypes() {
return {
children: PropTypes.element,
};
}
constructor(props, context) {
super(props, context);
this.state = {
menuState: OUT,
hamburgerState: OUT,
};
}
componentDidMount() {
if (this.props.router.location.pathname === '/') {
setTimeout(() => this.setState({ hamburgerState: SHOWBUTTON }), 6000);
} else {
this.setState({ hamburgerState: SHOWBUTTON });
}
// setTimeout(changeOffset, 8000);
}
completeF1Handler() {}
showMenu() {
this.setState({ menuState: IDLE });
}
hideMenu() {
this.setState({ menuState: HIDE });
}
reset() {
this.setState({ menuState: OUT });
}
render() {
const { stageWidth, stageHeight } = this.props;
return (
<ReactF1
className={styles.Header}
go={this.state.hamburgerState}
states={states(stageWidth, stageHeight)}
transitions={transitions()}
onComplete={() => this.completeF1Handler()}
>
<svg className={styles.hamburger} data-f1="hamburger" width="50" height="50">
<path
className={styles.triangle}
d="M0 0 L50 0 L0 50 Z"
onClick={this.showMenu.bind(this)}
fill={this.props.menuColor}
/>
</svg>
<Menu
go={this.state.menuState}
close={this.hideMenu.bind(this)}
reset={this.reset.bind(this)}
/>
</ReactF1>
);
}
}
locationform.js
import React, { Component, PropTypes } from 'react';
import { isDesktop } from '../../utils/device';
import LocateSelect from '../LocateSelect';
import styles from './LocationForm.css';
import buttonStyles from '../Button/Button.css';
// Menu states.
import { states, SHOWBUTTON, IDLE, HIDE, OUT } from '../Header/elements/Menu/states';
export default class LocationForm extends Component {
static get propTypes() {
return {
zipCode: PropTypes.string,
searchRadius: PropTypes.string,
businessType: PropTypes.string,
handleChange: PropTypes.func,
handleGeolocate: PropTypes.func,
handleSubmit: PropTypes.func,
};
}
constructor(props, context) {
super(props, context);
}
showMenu() {
console.log("show menu");
this.setState({ menuState: IDLE });
}
renderSelect() {
const { searchRadius, businessType, handleChange } = this.props;
return (
<div className={styles.selectContainer}>
<LocateSelect
id="searchRadius"
defaultValue=""
value={searchRadius}
handleChange={handleChange}
options={[
{
value: '',
text: 'SEARCH RADIUS',
},
{
value: '1',
text: '1 MI',
},
{
value: '5',
text: '5 MI',
},
{
value: '10',
text: '10 MI',
},
{
value: '25',
text: '25 MI',
},
]}
/>
<LocateSelect
id="businessType"
defaultValue=""
value={businessType}
handleChange={handleChange}
options={[
{
value: '',
text: 'BUSINESS TYPE',
},
{
value: 'bar',
text: 'Bar',
},
{
value: 'restaurant',
text: 'Restaurant',
},
{
value: 'liquorstore',
text: 'Liquor Store',
},
]}
/>
</div>
);
}
render() {
const {
zipCode,
handleChange,
handleSubmit,
handleGeolocate,
handleFocus,
handleBlur,
} = this.props;
return (
<form className={styles.LocationForm} onSubmit={handleSubmit}>
<input
type="button"
className={`${buttonStyles.Button} ${buttonStyles.dark} ${styles.geolocate}`}
value="Use Current Location"
onClick={handleGeolocate}
/>
<p>OR</p>
<input
id="zipCode"
type="text"
placeholder="ZIP CODE"
value={zipCode}
maxLength="5"
pattern="[0-9]*"
onFocus={handleFocus}
onBlur={handleBlur}
onChange={event => handleChange(event.target.id, event.target.value)}
/>
{this.renderSelect()}
<input
className={`${buttonStyles.Button} ${buttonStyles.dark}`}
type="submit"
value="search"
/>
<div className={buttonStyles.Button} onClick={() => this.showMenu()}>
No
</div>
</form>
);
}
}
With considering parent child communication in react you have to write a parent for both of these files and passing the function and the state as props .
For example :
app.js
import Header from './header.js'
import LocationForm from './location-form.js'
export default class App extends Component {
state = {menuState : 'out'}
showMenu() {
this.setState({ menuState:'idle' });
}
render(){
return(
<div>
<Header showMenu={this.showMenu} menuState={this.state.menuState}/>
<LocationForm showMenu={this.showMenu} menuState={this.state.menuState}/>
</div>
)
}
}
I think this concept of thinking will help you to reach the answer
So in react in order to pass data from parent to child elements you need to pass them down as props
So using your Menu component as an example you would pass the methods you want to use down to that child component like this: (I would recommend converting those to es6 syntax so you don't have to bother with binding).
//METHODS SECTION
const showMenu = () => {
this.setState({ menuState: IDLE });
}
const hideMenu = () => {
this.setState({ menuState: HIDE });
}
const reset = () => {
this.setState({ menuState: OUT });
}
//IN YOUR RENDER METHOD
<Menu
go={this.state.menuState}
close={this.hideMenu.bind(this)}
reset={this.reset.bind(this)}
showMenu={this.showMenu}
hideMenu={this.hideMenu}
reset={this.reset}
/>
Then in your child component you would refer to that method as this.props.showMenu or this.props.hideMenu or this.props.reset:
onClick(() => {this.props.showMenu()})
If you want the method to fire on an event you should wrap it in an anonymous function like I did above. You can also add it to another method within the child component that will get called when that method is called so:
const doSomething = () => {
count++
this.props.showMenu()
}
onClick(() => {this.doSomething()})
When you use the method in the child component it is still bound to the parent component. This is the main way of sending data back up to the parent. You could, for instance, have the method simply setState of a value on the parent component, pass the method down to the child as props with parameter requirements, pass the parameters in on the child and send it back up to parent when it is called. (Let me know if that was confusing)
There is a lot in your app that isn't very "React" so I would definitely recommend going through their documentation. Starting with: https://reactjs.org/docs/components-and-props.html and watching some videos on Youtube about state management in React.
Communication between parents is best thought of as having two methods of operating:
Parents can pass their children props
Children can call functions which are passed to them from their parents
This means that the relationship between components is defined via. their props. It's a one way method of communication that makes it much easier to build components as detached modules that happen to be connected in a given context.
So if you have something like:
Child A
/
Parent
\
Child B
Then you need to communicate via. the Parent. The children will call a function given to them by their parent, and the parent will pass down a new prop to the child components.
You can also do this thing via context, which allows you to avoid having to pass all of these props down manually, but does not affect the streams of communication available to components.
const ButtonComponent = (props) => {
return (
<div>
Child
<button onClick={props.handleClick}>
Do the thing
</button>
</div>
);
};
const CountDisplay = (props) => {
return (
<div>{props.count}</div>
);
};
const Parent = (props) => {
const parentLog = () => console.log('log from parent');
const [
currentCount,
setCurrentCount,
] = React.useState(0);
return (
<div>
Parent
<ButtonComponent handleClick={() => setCurrentCount(currentCount + 1)} />
<CountDisplay count={currentCount} />
</div>
);
};
ReactDOM.render(<Parent />, document.getElementById('app'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>

DRY react input handling causes cursor to jump

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

Customized Dropdown Field Component not compatible with Redux-Form

I have the following problem.
I have customized my own DropDown component using elements.
I want this element to interact with Redux-Form as I want to save the value that is selected.
This does not work:
<Field
name="name"
component={MyCustomizedDropDown}
data={myData}/>
The other option was to use the "input" props but as I am using elements, this is not possible.
Can someone give me a solution? Thanks.
MyCustomizedDropDown component:
import React, { Component } from "react";
import PropTypes from "prop-types";
class MyCustomizedDropdown extends Component {
constructor(props) {
super(props);
this.state = {
...this.props,
items: this.props.items || [],
selectedItem: this.props.items[0] || this.props.selectedItem,
showItems: false,
isOpened: false
};
this.dropDown = this.dropDown.bind(this);
this.selectedItem = this.selectedItem.bind(this);
}
dropDown() {
this.setState(prevState => ({
showItems: !prevState.showItems
}));
}
selectedItem(item) {
this.setState({
selectedItem: item,
showItems: false
});
}
render() {
const { input } = this.props;
return (
<div className="select-box--wrapper">
<div className="select-box--toggle" onClick={this.dropDown}>
<div className="select-box--selected-item">
{this.state.selectedItem && this.state.selectedItem.value}
</div>
<MyImage
className={`${
this.state.showItems
? "select-box--arrow-rotated"
: "select-box--arrow"
}`}
/>
</div>
<div className="select-box--main">
<div
{...input} \\THIS DOES NOT WORK
className="select-box--items">
{this.state.data.map(item => (
<div key={item.id} onClick={() => this.selectedItem(item)}>
{item.value}
</div>
))}
</div>
</div>
</div>
);
}
}
MyCustomizedDropdown.propTypes = {
data: PropTypes.array,
selectedItem: PropTypes.array,
input: PropTypes.object
};
export default MyCustomizedDropdown;
redux-form only works with "controlled" components. That means the component needs a prop that a parent uses to tell it what it's value is. For example, the following is a controlled component:
<TextField
value={this.state.inputValue}
onChange={(value) => this.setState({ inputValue: value })}
/>
Note that we're telling the TextField component what it's value is. You need to change your component to work the same way. The only caveat here is that redux-form injects a prop called input that is an object containing value and onChange (and a few other things), instead of directly injecting value and onChange.
So for the example above, it needs to work like this to support redux-form:
<TextField
input={{
value: this.state.inputValue,
onChange: (value) => this.setState({ inputValue: value })
}}
/>
Here's your component written as a "controlled" component, in a way that should work with redux-form:
import React, { Component } from "react";
import PropTypes from "prop-types";
class MyCustomizedDropdown extends Component {
constructor(props) {
super(props);
this.state = {
showItems: false
};
this.dropDown = this.dropDown.bind(this);
this.selectedItem = this.selectedItem.bind(this);
}
dropDown() {
this.setState(prevState => ({
showItems: !prevState.showItems
}));
}
hideDropdownItems() {
this.setState({
showItems: false
});
}
render() {
const { input } = this.props;
return (
<div className="select-box--wrapper">
<div className="select-box--toggle" onClick={this.dropDown}>
<div className="select-box--selected-item">
{this.input.value && this.input.value.value}
</div>
<MyImage
className={`${
this.state.showItems
? "select-box--arrow-rotated"
: "select-box--arrow"
}`}
/>
</div>
<div className="select-box--main">
<div
className="select-box--items">
{this.state.data.map(item => (
<div
key={item.id}
onClick={() => {
this.input.onChange(item)
this.hideDropdownItems();
}}
>
{item.value}
</div>
))}
</div>
</div>
</div>
);
}
}
MyCustomizedDropdown.propTypes = {
data: PropTypes.array,
selectedItem: PropTypes.array,
input: PropTypes.object
};
export default MyCustomizedDropdown;
Note we tell MyCustomizedDropdown what it's value is using this.props.input.value
We call this.props.input.onChange if the component wants to change it's value. Since it can't do it on it's own, it needs to tell it's parent it wants to change value.
The parent needs to respond to onChange and update MyCustomizedDropdown's value
For example, this is how you'd use your component without redux-form:
<MyCustomizedDropdown
input={{
value: this.state.dropDownValue,
onChange: (value) => this.setState({ dropDownValue: value })
}}
/>
And with redux-form, you can simply do the following, since redux-form manages all that stuff for you:
<Field
component={MyCustomizedDropdown}
/>
You shouldn't be handling the value of the input in the input's state. MyCustomizedDropDown should receive the handleChange function, items and selectedItem as props. The only thing that should be in the component's state is it's open or not.

Categories

Resources