81

I encountered this problem building a webapp and I replicated it in this jsfiddle. Essentially, I would like an input to call this.setState({message: input_val}) every time I type something into it, then pass it into the parent App class which then re-renders the message onto the Message class. However the output seems to always be one step behind what I actually type. The jsfiddle demo should be self explanatory. I am wondering if I did anything wrong to prompt this.

html

<script src="http://facebook.github.io/react/js/jsfiddle-integration.js"></script>
<div id='app'></div>

js

var App = React.createClass({
    getInitialState: function() {
        return {message: ''}
    },
    appHandleSubmit: function(state) {
        this.setState({message: state.message});
        console.log(this.state.message);
    },
    render: function() {
        return (
            <div className='myApp'>
            <MyForm onChange={this.appHandleSubmit}/>
            <Message message={this.state.message}/>
            </div>
        );
    }
});

var MyForm = React.createClass({
    handleSubmit: function() {
        this.props.onChange(this.state);
    },
    handleChange: function(e) {
        console.log(e.target.value);
        this.setState({message: e.target.value});
        this.handleSubmit();
    },
    render: function() {
        return (
            <form className="reactForm" onChange={this.handleChange}>
            <input type='text' />
            </form>
        );
    }
});

var Message = React.createClass({
    render: function() {
        return (
            <div className="message">
                <p>{this.props.message}</p>
            </div>
        )
    }
});

React.render(
    <App />,
    document.getElementById('app')
);
Keith Yong
  • 1,474
  • 1
  • 15
  • 20

10 Answers10

90

A call to setState isn't synchronous. It creates a "pending state transition." (See here for more details). You should explicitly pass the new input value as part of the event being raised (like to handleSubmit in your example).

See this example.

handleSubmit: function(txt) {
    this.props.onChange(txt);
},
handleChange: function(e) {
    var value = e.target.value;
    this.setState({message: value});
    this.handleSubmit(value);
},
WiredPrairie
  • 58,954
  • 17
  • 116
  • 143
38

There is a much simpler way to do this, setState(updater, callback) is an async function and it takes the callback as second argument,

Simply pass the handleSubmit as a callback to setState method, this way after setState is complete only handleSubmit will get executed.

For eg.

handleChange: function(e) {
    console.log(e.target.value);
    this.setState({message: e.target.value}, this.handleSubmit);
}

Try to change the handleChange() method like above and it will work.

for syntax of using setState check this link

rc-chandan
  • 601
  • 5
  • 10
  • +1 This is by far the most sensible answer I think. You could've mentioned the alternative with componentDidUpdate too ;) – Kjellski Sep 11 '17 at 14:30
9

with setState hook

useEffect(() => {
    your code...
}, [yourState]);
Ahmed Boutaraa
  • 1,908
  • 1
  • 12
  • 10
  • 3
    Could you please expand this answer? – burntsugar Dec 05 '20 at 01:49
  • useEffect is a lifecycle management hook for functional comps as opposed to the old classes. the docs are pretty straight fwd if u've ever used hooks. if not learn up. basically that code will run whenever the variable/param "yourState" changes, not before its updated (leaving u one step behind) – sao Feb 04 '21 at 16:06
8

I was pulling my hair out for like an hour because of this so I decided to share... If your callback is still one step behind and seemingly not working, ensure you don't CALL the function with parenthesis... Just pass it in. Rookie mistake.

RIGHT:

handleChange: function(e) {
    console.log(e.target.value);
    this.setState({message: e.target.value}, this.handleSubmit);
}

VS

WRONG:

handleChange: function(e) {
    console.log(e.target.value);
    this.setState({message: e.target.value}, this.handleSubmit());
}
Taylor A. Leach
  • 2,115
  • 4
  • 25
  • 43
  • 1
    Or of course, you can do `this.setState({message: e.target.value}, () => this.handleSubmit());` or `handleSubmit.bind(...)` if you have to pass different arguments into handleSubmit. – Ben J Mar 06 '19 at 05:35
5

There's no reason for MyForm to be using state here. Also putting the onChange on the form instead of the input you're interested in is odd. Controlled components should be preferred because their behavior is more obvious, and any time App's message state changes (even if you e.g. allow Message to change it later), it'll be correct everywhere.

This also makes your code a bit shorter, and considerably simpler.

var App = React.createClass({
    getInitialState: function() {
        return {message: ''}
    },
    appHandleSubmit: function(message) {
        this.setState({message: message});
    },
    render: function() {
        return (
            <div className='myApp'>
                <MyForm onChange={this.appHandleSubmit} 
                        message={this.state.message} />
                <Message message={this.state.message}/>
            </div>
        );
    }
});

var MyForm = React.createClass({
    handleInputChange: function(e){
        this.props.onChange(e.target.value);
    },
    // now always in sync with the parent's state
    render: function() {
        return (
            <form className="reactForm">
                <input type='text' onChange={this.handleInputChange}
                       value={this.props.message} />
            </form>
        );
    }
});

jsbin

Brigand
  • 84,529
  • 20
  • 165
  • 173
  • Thanks for the refactor, I studied your code and took some good points from it. Seems like using this.props is a good idea for pure views, and using this.state is good for controllers. – Keith Yong Feb 28 '15 at 01:19
  • Mostly, yes. Occasionally there's something like a dropdown, where the open/closed state can be kept private to the component (why would you ever care if the dropdown's open in the parent?). Also sometimes UI components need to track something like the cursor position and use it in render, so that ends up in their local state. – Brigand Feb 28 '15 at 01:29
  • FYI -- this code doesn't work as is. `onChange` doesn't transmit the actual text of the input box as a value as you've done there (a `SyntheticEvent` is actually passed). I also wouldn't expect that a container should know anything about the specifics of how a child component is rendered, so it shouldn't use `e.target.value` for example to extract the value from the input box. That's why having a level of indirection is very useful in something like `MyForm` in the original code. – WiredPrairie Feb 28 '15 at 01:37
4

Knowing the problem is with not having asyncronous behaviour of setState I solved my issue with async await

onChangeEmail=async x =>{
await this.setState({email:x})
}
Redmen Ishab
  • 2,199
  • 18
  • 22
3

You could refactor your class-based component to a functional component as someone else mentioned. The drawbacks are that this can be quite time-consuming depending on how many code lines you have to refactor and is likely prone to error.

I will use your example of a changeHandler to display how it could be done in a functional component.

const INITIAL_DATA = {
  message: ""
}
    
const [form, setForm] = useState({...INITIAL_DATA})

const changeHandler = (e) = setForm({
  ...form,
  [e.target.name]: e.target.value
})

<InputField name={message} value={form.message} onChange={changeHandler}>

^ The above code will produce that behavior as you explained of onChange being one step behind the setState. As others have said, this is due to the asynchronous nature of the setState hook.

The way to solve this is to use the useEffect hook, which allows you to introduce state dependencies. So when setState is finished going through its update cycle the useEffect will fire. So add the below to the above code and BAM.. it should show the state changes without a step delay.

useEffect(() => {
  doSomeValidationHere()
  orSendOffTheForm()
}, [form])

*Notice how we add a dependency array after the useEffect(() => {}) hook.

Extra info:

  • If the dependency array is empty it will only run on component mount and first render
  • If there is no array, it will run every time the page renders
  • If there is a state name in the array it will run every time that state is finished setting
ajsaule
  • 146
  • 1
  • 7
  • Thank you for showing an example with `useEffect()` for a functional component. This was the most helpful answer for me! – Gabrien Jan 03 '23 at 15:55
2

I found it very cumbersome for me to define 3 handler functions just to get some value to a component's state, so I decided not to use state at all. I just defined an additional property to the component that stored desired value.

So I ended up with a code that looked something like this:

//...
},
text: '',
handleChange: function(event) {
  this.text = event.target.value;
  this.forceUpdate();
},
render: function() {
    return <div>
      <InputComponent onChange={this.handleChange}/>
      <DisplayComponent textToDisplay={this.text}/>
      </div>
}
//...
2

or as in my case - just use onKeyUp, instead of down...

0

There are some solutions mentions above, some of them have some problems, but ajsaule is the correct ansower. Here is show my Code in TypeScript Example:

    // solve the problem: setstate always one step behind
    useEffect(() => { isFormValid(fields, setFieldsError) }, [fields])
    const handleChange = (field: string ) => (evt: React.ChangeEvent<HTMLInputElement>) => {
        setFields({ ...fields, [field]: evt.target.value })
    }


const isFormValid = (fields: FieldsState, setFieldsError: React.Dispatch<React.SetStateAction<TempObj>>) => {
    const tempObj: TempObj = {}
    const { email, password } = fields

    if( !isEmail(email) ) tempObj.email = 'Invalid Email Address'
    if( password.length < 8 ) tempObj.password = 'password must by atleast 8 character long'

    Object.keys(fields).forEach(field => {
        if(!fields[field as keyof FieldsState]) tempObj[field] = `'${field}' is emapty`
    })
    setFieldsError(tempObj)

    return Object.values(tempObj).every( item => item == '' )
}