4

I made a code sandbox example for my problem: https://codesandbox.io/s/react-form-submit-problem-qn0de. Please try to click the "+"/"-" button on both Function Example and Class Example and you'll see the difference. On the Function Example, we always get the previous value while submitting.

I'll explain details about this example below.

We have a react component like this

function Counter(props) {
  return (
    <>
      <button type="button" onClick={() => props.onChange(props.value - 1)}>
        -
      </button>
      {props.value}
      <button type="button" onClick={() => props.onChange(props.value + 1)}>
        +
      </button>
      <input type="hidden" name={props.name} value={props.value} />
    </>
  );
}

It contains two buttons and a numeric value. User can press the '+' and '-' button to change the number. It also renders an input element so we can use it in a <form>.

This is how we use it

class ClassExample extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      value: 1,
      lastSubmittedQueryString: ""
    };

    this.formEl = React.createRef();
  }

  handleSumit = () => {
    if (this.formEl.current) {
      const formData = new FormData(this.formEl.current);
      const search = new URLSearchParams(formData);
      const queryString = search.toString();
      this.setState({
        lastSubmittedQueryString: queryString
      });
    }
  };

  render() {
    return (
      <div className="App">
        <h1>Class Example</h1>
        <form
          onSubmit={event => {
            event.preventDefault();
            this.handleSumit();
          }}
          ref={ref => {
            this.formEl.current = ref;
          }}
        >
          <Counter
            name="test"
            value={this.state.value}
            onChange={newValue => {
              this.setState({ value: newValue }, () => {
                this.handleSumit();
              });
            }}
          />
          <button type="submit">submit</button>
          <br />
          lastSubmittedQueryString: {this.state.lastSubmittedQueryString}
        </form>
      </div>
    );
  }
}

We render our <Counter> component in a <form>, and want to submit this form right after we change the value of <Counter>. However, on the onChange event, if we just do

onChange={newValue => {
  this.setState({ value: newValue });
  this.handleSubmit();
}}

then we won't get the updated value, probably because React doesn't run setState synchronously. So instead we put this.handleSubmit() in the second argument callback of setState to make sure it is executed after the state has been updated.

But in the Function Example, as far as I know in state hooks there's nothing like the second argument callback function of setState. So we cannot achieve the same goal. We found out two workarounds but we are not satisfied with either of them.

Workaround 1

We tried to use the effect hook to listen when the value has been changed, we submit our form.

React.useEffect(() => {
  handleSubmit();
}, [value])

But sometimes we need to just change the value without submitting the form, we want to invoke the submit event only when we change the value by clicking the button, so we think it should be put in the button's onChange event.

Workaround 2

onChange={newValue => {
  setValue(newValue);
  setTimeout(() => {
    handleSubmit();
  })
}}

This works fine. We can always get the updated value. But the problem is we don't understand how and why it works, and we never see people write code in this way. We are afraid if the code would be broken with the future React updates.

Sorry for the looooooooong post and thanks for reading my story. Here are my questions:

  1. How about Workaround 1 and 2? Is there any 'best solution' for the Function Example?
  2. Is there anything we are doing wrong? For example maybe we shouldn't use the hidden input for form submitting at all?

Any idea will be appreciated :)

ssdh233
  • 145
  • 1
  • 11
  • 1
    Workaround 1 isn't a workaround. It is the proper way to do it. In useEffect, the second argument is an array with its dependencies. – hdotluna Jan 10 '20 at 08:09
  • 1
    Does this answer your question? [How to use \`setState\` callback on react hooks](https://stackoverflow.com/questions/56247433/how-to-use-setstate-callback-on-react-hooks) –  Jan 10 '20 at 08:31
  • The only problem of Workaround 1 is that instead of associating the submit with click event, it associates the submit with the state change. Sometime we might need to change the state without submitting the form (I know the requirement sounds really weird and it probably wouldn't happen in real). – ssdh233 Jan 10 '20 at 08:45

2 Answers2

0

Can you call this.handleSubmit() in componentDidUpdate()?

Since your counter is binded to the value state, it should re-render if there's a state change.

componentDidUpdate(prevProps, prevState) {
  if (this.state.value !== prevState.value) {
    this.handleSubmit();
  }
}

This ensure the submit is triggered only when the value state change (after setState is done)

Andus
  • 1,713
  • 13
  • 30
0

It's been a while. After reading React 18's update detail, I realize the difference is caused by React automatically batching state updates in function components, and the "official" way to get rid of it is to use ReactDOM.flushSync().

import { flushSync } from "react-dom";

onChange={newValue => {
  flushSync(() => {
    setValue(newValue)
  });
  flushSync(() => {
    handleSubmit();
  });
}}
ssdh233
  • 145
  • 1
  • 11