What's the react way of setting focus on a particular text field after the component is rendered?
Documentation seems to suggest using refs, e.g:
Set ref="nameInput" on my input field in the render function, and then call:
this.refs.nameInput.getInputDOMNode().focus();
But where should I call this? I've tried a few places but I cannot get it to work.
#Dhiraj's answer is correct, and for convenience you can use the autoFocus prop to have an input automatically focus when mounted:
<input autoFocus name=...
Note that in jsx it's autoFocus (capital F) unlike plain old html which is case-insensitive.
You should do it in componentDidMount and refs callback instead. Something like this
componentDidMount(){
this.nameInput.focus();
}
class App extends React.Component{
componentDidMount(){
this.nameInput.focus();
}
render() {
return(
<div>
<input
defaultValue="Won't focus"
/>
<input
ref={(input) => { this.nameInput = input; }}
defaultValue="will focus"
/>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.js"></script>
<div id="app"></div>
Focus on mount
If you just want to focus an element when it mounts (initially renders) a simple use of the autoFocus attribute will do.
<input type="text" autoFocus />
Dynamic focus
to control focus dynamically use a general function to hide implementation details from your components.
React 16.8 + Functional component - useFocus hook
const FocusDemo = () => {
const [inputRef, setInputFocus] = useFocus()
return (
<>
<button onClick={setInputFocus} >
Focus
</button>
<input ref={inputRef} />
</>
)
}
const useFocus = () => {
const htmlElRef = useRef(null)
const setFocus = () => {htmlElRef.current && htmlElRef.current.focus()}
return [ htmlElRef, setFocus ]
}
Full Demo
React 16.3 + Class Components - utilizeFocus
class App extends Component {
constructor(props){
super(props)
this.inputFocus = utilizeFocus()
}
render(){
return (
<>
<button onClick={this.inputFocus.setFocus}>
Focus
</button>
<input ref={this.inputFocus.ref}/>
</>
)
}
}
const utilizeFocus = () => {
const ref = React.createRef()
const setFocus = () => {ref.current && ref.current.focus()}
return {setFocus, ref}
}
Full Demo
As of React 0.15, the most concise method is:
<input ref={input => input && input.focus()}/>
If you just want to make autofocus in React, it's simple.
<input autoFocus type="text" />
While if you just want to know where to put that code, answer is in componentDidMount().
v014.3
componentDidMount() {
this.refs.linkInput.focus()
}
In most cases, you can attach a ref to the DOM node and avoid using findDOMNode at all.
Read the API documents here: https://facebook.github.io/react/docs/top-level-api.html#reactdom.finddomnode
React 16.3 added a new convenient way to handle this by creating a ref in component's constructor and use it like below:
class MyForm extends Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focus();
}
render() {
return(
<div>
<input ref={this.textInput} />
</div>
);
}
}
For more details about React.createRef, you can check this article in React blog.
Update:
Starting from React 16.8, useRef hook can be used in function components to achieve the same result:
import React, { useEffect, useRef } from 'react';
const MyForm = () => {
const textInput = useRef(null);
useEffect(() => {
textInput.current.focus();
}, []);
return (
<div>
<input ref={textInput} />
</div>
);
};
The React docs now have a section for this. https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute
render: function() {
return (
<TextInput
ref={function(input) {
if (input != null) {
input.focus();
}
}} />
);
},
I just ran into this issue and I'm using react 15.0.1 15.0.2 and I'm using ES6 syntax and didn't quite get what I needed from the other answers since v.15 dropped weeks ago and some of the this.refs properties were deprecated and removed.
In general, what I needed was:
Focus the first input (field) element when the component mounts
Focus the first input (field) element with an error (after submit)
I'm using:
React Container/Presentation Component
Redux
React-Router
Focus the First Input Element
I used autoFocus={true} on the first <input /> on the page so that when the component mounts, it will get focus.
Focus the First Input Element with an Error
This took longer and was more convoluted. I'm keeping out code that isn't relevant to the solution for brevity.
Redux Store / State
I need a global state to know if I should set the focus and to disable it when it was set, so I don't keep re-setting focus when the components re-render (I'll be using componentDidUpdate() to check for setting focus.)
This could be designed as you see fit for you application.
{
form: {
resetFocus: false,
}
}
Container Component
The component will need to have the resetfocus property set and a callBack to clear the property if it ends up setting focus on itself.
Also note, I organized my Action Creators into separate files mostly due to my project is fairly large and I wanted to break them up into more manageable chunks.
import { connect } from 'react-redux';
import MyField from '../presentation/MyField';
import ActionCreator from '../actions/action-creators';
function mapStateToProps(state) {
return {
resetFocus: state.form.resetFocus
}
}
function mapDispatchToProps(dispatch) {
return {
clearResetFocus() {
dispatch(ActionCreator.clearResetFocus());
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MyField);
Presentation Component
import React, { PropTypes } form 'react';
export default class MyField extends React.Component {
// don't forget to .bind(this)
constructor(props) {
super(props);
this._handleRef = this._handleRef.bind(this);
}
// This is not called on the initial render so
// this._input will be set before this get called
componentDidUpdate() {
if(!this.props.resetFocus) {
return false;
}
if(this.shouldfocus()) {
this._input.focus();
this.props.clearResetFocus();
}
}
// When the component mounts, it will save a
// reference to itself as _input, which we'll
// be able to call in subsequent componentDidUpdate()
// calls if we need to set focus.
_handleRef(c) {
this._input = c;
}
// Whatever logic you need to determine if this
// component should get focus
shouldFocus() {
// ...
}
// pass the _handleRef callback so we can access
// a reference of this element in other component methods
render() {
return (
<input ref={this._handleRef} type="text" />
);
}
}
Myfield.propTypes = {
clearResetFocus: PropTypes.func,
resetFocus: PropTypes.bool
}
Overview
The general idea is that each form field that could have an error and be focused needs to check itself and if it needs to set focus on itself.
There's business logic that needs to happen to determine if the given field is the right field to set focus to. This isn't shown because it will depend on the individual application.
When a form is submitted, that event needs to set the global focus flag resetFocus to true. Then as each component updates itself, it will see that it should check to see if it gets the focus and if it does, dispatch the event to reset focus so other elements don't have to keep checking.
edit
As a side note, I had my business logic in a "utilities" file and I just exported the method and called it within each shouldfocus() method.
Cheers!
This is not longer the best answer. As of v0.13, this.refs may not available until AFTER componentDidMount() runs, in some odd cases.
Just add the autoFocus tag to your input field, as FakeRainBrigand showed above.
Ref. #Dave's comment on #Dhiraj's answer; an alternative is to use the callback functionality of the ref attribute on the element being rendered (after a component first renders):
<input ref={ function(component){ React.findDOMNode(component).focus();} } />
More info
Using React Hooks / Functional components with Typescript, you can use the useRef hook with HTMLInputElement as the generic parameter of useRef:
import React, { useEffect, useRef } from 'react';
export default function MyComponent(): JSX.Element {
const inputReference = useRef<HTMLInputElement>(null);
useEffect(() => {
inputReference.current?.focus();
}, []);
return (
<div>
<input ref={inputReference} />
</div>
);
}
Or if using reactstrap, supply inputReference to innerRef instead of ref:
import React, { useEffect, useRef } from 'react';
import { Input } from 'reactstrap';
export default function MyComponent(): JSX.Element {
const inputReference = useRef<HTMLInputElement>(null);
useEffect(() => {
inputReference.current?.focus();
}, []);
return (
<div>
<Input innerRef={inputReference} />
</div>
);
}
Note that none of these answers worked for me with a material-ui TextField component. Per How to set focus to a materialUI TextField? I had to jump through some hoops to get this to work:
const focusUsernameInputField = input => {
if (input) {
setTimeout(() => {input.focus()}, 100);
}
};
return (
<TextField
hintText="Username"
floatingLabelText="Username"
ref={focusUsernameInputField}
/>
);
This is the proper way, how to autofocus. When you use callback instead of string as ref value, it is automatically called. You got your ref available than without the need of touching the DOM using getDOMNode
render: function() {
return <TextInput ref={(c) => this._input = c} />;
},
componentDidMount: function() {
this._input.focus();
},
You don't need getInputDOMNode?? in this case...
Just simply get the ref and focus() it when component gets mounted -- componentDidMount...
import React from 'react';
import { render } from 'react-dom';
class myApp extends React.Component {
componentDidMount() {
this.nameInput.focus();
}
render() {
return(
<div>
<input ref={input => { this.nameInput = input; }} />
</div>
);
}
}
ReactDOM.render(<myApp />, document.getElementById('root'));
You can put that method call inside the render function. Or inside the life cycle method, componentDidUpdate
I have same problem but I have some animation too, so my colleague suggest to use window.requestAnimationFrame
this is ref attribute of my element:
ref={(input) => {input && window.requestAnimationFrame(()=>{input.focus()})}}
AutoFocus worked best for me. I needed to change some text to an input with that text on double click so this is what I ended up with:
<input autoFocus onFocus={this.setCaretToEnd} value={this.state.editTodo.value} onDoubleClick={this.updateTodoItem} />
NOTE: To fix the issue where React places the caret at the beginning of the text use this method:
setCaretToEnd(event) {
var originalText = event.target.value;
event.target.value = '';
event.target.value = originalText;
}
Found here:
https://coderwall.com/p/0iz_zq/how-to-put-focus-at-the-end-of-an-input-with-react-js
<input type="text" autoFocus />
always try the simple and basic solution first, works for me.
To move focus to a newly created element, you can store the element's ID in the state and use it to set autoFocus. e.g.
export default class DefaultRolesPage extends React.Component {
addRole = ev => {
ev.preventDefault();
const roleKey = this.roleKey++;
this::updateState({
focus: {$set: roleKey},
formData: {
roles: {
$push: [{
id: null,
name: '',
permissions: new Set(),
key: roleKey,
}]
}
}
})
}
render() {
const {formData} = this.state;
return (
<GridForm onSubmit={this.submit}>
{formData.roles.map((role, idx) => (
<GridSection key={role.key}>
<GridRow>
<GridCol>
<label>Role</label>
<TextBox value={role.name} onChange={this.roleName(idx)} autoFocus={role.key === this.state.focus}/>
</GridCol>
</GridRow>
</GridSection>
))}
</GridForm>
)
}
}
This way none of the textboxes get focus on page load (like I want), but when you press the "Add" button to create a new record, then that new record gets focus.
Since autoFocus doesn't "run" again unless the component gets remounted, I don't have to bother unsetting this.state.focus (i.e. it won't keep stealing focus back as I update other states).
Simple solution without autofocus:
<input ref={ref => ref && ref.focus()}
onFocus={(e)=>e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length)}
/>
ref triggers focus, and that triggers onFocus to calculate the end and set the cursor accordingly.
Ben Carp solution in typescript
React 16.8 + Functional component - useFocus hook
export const useFocus = (): [React.MutableRefObject<HTMLInputElement>, VoidFunction] => {
const htmlElRef = React.useRef<HTMLInputElement>(null);
const setFocus = React.useCallback(() => {
if (htmlElRef.current) htmlElRef.current.focus();
}, [htmlElRef]);
return React.useMemo(() => [htmlElRef, setFocus], [htmlElRef, setFocus]);
};
Warning: ReactDOMComponent: Do not access .getDOMNode() of a DOM node; instead, use the node directly. This DOM node was rendered by App.
Should be
componentDidMount: function () {
this.refs.nameInput.focus();
}
The simplest answer is add the ref="some name" in the input text element and call the below function.
componentDidMount(){
this.refs.field_name.focus();
}
// here field_name is ref name.
<input type="text" ref="field_name" />
After trying a lot of options above with no success I've found that It was as I was disabling and then enabling the input which caused the focus to be lost.
I had a prop sendingAnswer which would disable the Input while I was polling the backend.
<Input
autoFocus={question}
placeholder={
gettingQuestion ? 'Loading...' : 'Type your answer here...'
}
value={answer}
onChange={event => dispatch(updateAnswer(event.target.value))}
type="text"
autocomplete="off"
name="answer"
// disabled={sendingAnswer} <-- Causing focus to be lost.
/>
Once I removed the disabled prop everything started working again.
Read almost all the answer but didnt see a getRenderedComponent().props.input
Set your text input refs
this.refs.username.getRenderedComponent().props.input.onChange('');
According to the updated syntax, you can use this.myRref.current.focus()
Focus using createRef for functional components
To developers using Functional Components. This seems to suit. Focus happens on inputfield after clicking on the button. I've attached CodeSandbox link too.
import React from 'react';
export default function App() {
const inputRef = React.createRef();
return <>
<input ref={inputRef} type={'text'} />
<button onClick={() => {if (inputRef.current) { inputRef.current.focus() }}} >
Click Here
</button>
</>
}
https://codesandbox.io/s/blazing-http-hfwp9t
That one worked for me:
<input autoFocus={true} />
Updated version you can check here
componentDidMount() {
// Focus to the input as html5 autofocus
this.inputRef.focus();
}
render() {
return <input type="text" ref={(input) => { this.inputRef = input }} />
})
Since there is a lot of reasons for this error I thought that I would also post the problem I was facing. For me, problem was that I rendered my inputs as content of another component.
export default ({ Content }) => {
return (
<div className="container-fluid main_container">
<div className="row">
<div className="col-sm-12 h-100">
<Content /> // I rendered my inputs here
</div>
</div>
</div>
);
}
This is the way I called the above component:
<Component Content={() => {
return (
<input type="text"/>
);
}} />
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.
I am writing a ControlledInput component, and in order to have access to the state of the component using ControlledInput, I have a binder prop in ControlledInput.
I'm having a slight issue when using the component:
render() {
const CI = props => <ControlledInput binder={this} {...props} />;
return (
<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput binder={this} label={"Title"} />
</div>
);
}
The implementation above works completely fine. However, note the const CI I've defined. I tried to use this so I could just write <CI label={"Title"}/> without the binder since the binder will be the same on all the ControlledInput components I use in a given render method.
The problem with using <CI label={"Title"}/> is that when I type into the input, the input "blurs" and I have to reselect it. This appears to be because the render method creates the CI on every render.
I hope I've explained that clearly, because my head hurts.
Anyway, it makes sense to me why this happens. And I know that one solution is to put const CI = props => <ControlledInput binder={this} {...props} />; outside of the render function. But then I'd have to call it as <this.CI> and that starts to defeat the purpose.
And I can't put CI in global scope because then I don't have access to this.
Is there a way to solve this?
Update
Here is the current (very much in progress) code for ControlledInput:
// #flow
import React, { Component } from "react";
type Props = {
containerStyle?: Object,
label: string,
propName?: string,
binder: Component<Object, Object>,
onChange?: Object => void
};
class ControlledInput extends Component<Props> {
render() {
const props = this.props;
const propName = props.propName || props.label.toLowerCase();
return (
<div style={props.containerStyle}>
<p>{props.label}</p>
<input
type="text"
label={props.label}
onChange={
this.props.onChange ||
(e => {
props.binder.setState({ [propName]: e.target.value });
})
}
value={props.binder.state[propName]}
></input>
</div>
);
}
}
The point of this whole endeavor is to simplify creating a form with controlled components, avoiding having to add value={this.state.whatever} and onChange={e=>this.setState({whatever: e})} to each one, which is not DRY in my opinion.
And then I want get a little more DRY by not passing binder={this} to every component and that's why I'm doing const CI = props => <ControlledInput binder={this} {...props} />;, which, again, has to be inside the class to access this and inside the render function to be called as CI rather than this.CI.
So that first explanation why you need to pass this, although I suppose I could also have props like setState={this.setState} parentState={this.state}, and in that case it does indeed start to make sense to combine those into something like {...propsToSend} as #John Ruddell suggested.
Note that I've provided a possibility to override onChange, and plan on doing so for most or all of the other props (e.g, value={this.props.value || binder.state[propName]}. If one were to override a lot of these (especially value and onChange) it would indeed make the component much less reusable, but the main use case is for quickly creating multiple inputs that don't have special input handling.
So, again, my ideal would be to call <ControlledInput label="Title"/> and have the component code take care of binding state and setState correctly. If this is possible. And then the second option would be to have a place to define the necessary context props in a place that makes it simple when it's time to actually use the component multiple times, like so:
<ControlledInput label={"title"} {...contextProps}/>
<ControlledInput label={"author"} {...contextProps}/>
<ControlledInput label={"email"} {...contextProps}/>
<ControlledInput label={"content"} textArea={true} {...contextProps}/> // textarea prop not implemented yet, fyi
etc
I hear that accessing the parent state/context may be an anti-pattern, but there must be some way to do what I'm trying to do without using an anti-pattern, isn't there?
If you want the state of the parent, handle the state there and pass down the value to your input - ControlledInput won't have to know anything except how to handle data in and out. Something like this, and note that I jacked up the names a little so you can see which component is handling what:
import React, { useState } from "react"
const Parent = () => {
const [title, setTitle] = useState("")
const handleChangeInParent = (newTitle) => {
setTitle((oldValue) => newTitle)
}
return(<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput handleChange={handleChangeInParent} label={title} />
</div>)
}
const ControlledInput = ({handleChange, label}) => {
return (
<input onChange={handleChange} type="text" value={label} />
)
}
If ControlledComponent needs to handle its own state, then pass it a default value and then have the Parent read the value when saving (or whatever):
import React, { useState } from "react"
const Parent = () => {
const handleSaveInParent = (newTitle) => {
console.log("got the new title!")
}
return (
<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput handleSave={handleSaveInParent} initialLabel="Title" />
</div>
)
}
const ControlledInput = ({ handleSave, initialLabel }) => {
const [title, setTitle] = useState(initialLabel)
const handleChange = (ev) => {
const value = ev.target.value
setTitle((oldValue) => value)
}
const handleSubmit = (ev) => {
ev.preventDefault()
handleSave(title)
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} type="text" value={title} />
</form>
)
}
You shouldn't be sending this through - just send values and/or functions to handle values.
With Updated Implementation
(okay, John you win!)
Not positive if this is technically an "answer", but I've rewritten the component to take a state and (updated) a setterFn prop:
component
// #flow
import React, { Component } from "react";
type Props = {
containerStyle?: Object,
labelStyle?: Object,
label: string,
propName?: string,
state: Object,
onChange?: Object => void,
textArea?: boolean,
setterFn: (key: string, value: mixed) => void
};
class ControlledInput extends Component<Props> {
render() {
const props = this.props;
const propertyName = props.propName || props.label.toLowerCase();
const TagType = props.textArea ? "textarea" : "input";
// only pass valid props to DOM element (remove any problematic custom props)
const { setterFn, propName, textArea, ...domProps } = props;
return (
<div style={props.containerStyle}>
<p style={props.labelStyle}>{props.label}</p>
<TagType
{...domProps}
label={props.label} // actually could get passed automatically, but it's important so I'm leaving it in the code
onChange={
this.props.onChange ||
(setterFn ? e => setterFn(propertyName, e.target.value) : null)
}
value={props.state[propertyName] || ""}
></TagType>
</div>
);
}
}
export default ControlledInput;
in use (somehow less code than before!)
class Wrapper extends Component<Object, Object> {
state = {};
render() {
const setterFn = (k, v) => this.setState({ [k]: v });
const p = { state: this.state, setterFn: setterFn.bind(this) };
return <ControlledInput {...p} {...this.props.inputProps} />
}
}
I guess this is more appropriate. It still takes up a lot more space than binder={this}.
It doesn't actually the questions of:
How to access the parent's state from the component. Though from comments it seems like this is an anti-pattern, which I do understand from the theory of React.
How to set these repeating props elsewhere so that I can just call `. I guess the only solution is to do something like this:
render() {
const props = {state: this.state, setState: this.setState}
<ControlledInput {...props} label="Title"/>
}
Which certainly isn't such a bad solution. Especially if I shorten that name to, say, a single character.
Much thanks to #John Ruddell for setting me on the right path.
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))}