Skip to content

Different behavior compared to native react.js: state sudden reset #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
stillwaiting opened this issue Mar 17, 2016 · 6 comments
Closed
Labels

Comments

@stillwaiting
Copy link

Hi. I tried to translate this simple code into scalajs-react:

/* Javascript */

// Just a plain Input object which saves user's input as a state
var Input = React.createClass({
    getInitialState: function() { return { value: '' } },

    onChanged: function(e) {
        this.setState({ value: e.target.value });
    },

    render: function() {
               // <input type="text" value={this.state.value} onChange={this.onChanged} />
        return React.createElement("input", {type: "text", value: this.state.value, onChange: this.onChanged});
    }
});

// Displays a number and an <Input> next to it
var TestCounter = React.createClass({
    render: function() {
                // <div>
                //    <span>{this.props.counter}</span>
                //    <Input />
               // </div>
        return React.createElement("div", {}, 
              React.createElement("span", {}, this.props.counter),
              React.createElement(Input)
         );
    }
});

// Launch the counter
var cnt = 0;
setInterval(function(){
    ReactDOM.render(React.createElement(TestCounter, {counter: cnt}), domNode);
    cnt ++;
}, 1000);

And the result:

/* Scala */

// Just a plain Input object which saves user's input as a state
class InputBackend($: BackendScope[Unit, String]) {
    def onChanged(e: ReactEventI): Callback ={
      $.modState((_) => {e.target.value} );
    }

    def render() = {
      <.input (
        ^.`type` := "text",
        ^.value := $.state.runNow(),
        ^.onChange ==> onChanged
      );
    }
  }

// Displays a number and an <Input> next to it
  class TextCounterBackend($: BackendScope[Int, Unit]) {
    def render() = {
      val Child = ReactComponentB[Unit]("Input")
        .initialState("this string wipes out any change in the input")
        .renderBackend[InputBackend]
        .build

      <.div(
        <.span($.props.runNow()),
        Child(Unit)
      );
    }
  }

  var cnt = 0;

// Launch the counter
  @JSExport
  override def main(): Unit = {
    setInterval(1000) {
      ReactDOM.render(
        ReactComponentB[Int]("TextCounter")
          .renderBackend[TextCounterBackend]
          .build(cnt),
        dom.document.getElementById("test_javascript_scala")
      );
      cnt += 1;
    };
  }

However, the result in the browser behaves differently from the original one:

  • in the first case (javascript) the counter is ticking and I can seamlessly modify the value of the input, the change is preserved from tick to tick;
  • in the 2nd case (scalajs-react) the counter is ticking, but any change I introduce is dropped to the initial one on each next tick.

Am I doing something wrong here?

@stillwaiting stillwaiting changed the title Different behavior against native implementation: state sudden reset Different behavior compared to native implementation: state sudden reset Mar 17, 2016
@stillwaiting stillwaiting changed the title Different behavior compared to native implementation: state sudden reset Different behavior compared to native react.js: state sudden reset Mar 17, 2016
@japgolly
Copy link
Owner

They're different (hi btw) in that unlike the JS version, in your Scala version you're creating the component inside the tick-loop. Which means React keeps thinking it's got new components each tick.

Move this:

ReactComponentB[Int]("TextCounter")
  .renderBackend[TextCounterBackend]
  .build

to the top level, outside of the loop.

A common structure I use is to have an object with Props and/or State and/or Backend types defined inside, and a val Component = ReactComponentB ... .build so that everything pertaining to the component is all in one unit.

Like:

object TextCounter {
  type Props = Int
  type State = Unit
  final class Backend($: BackendScope[Props, State]) {
    ...
  }
  val Component = ...
}

@stillwaiting
Copy link
Author

That was really fast and helpful 😄 Thanks a lot, let me try that. 👍

@stillwaiting
Copy link
Author

Unfortunately didn't work, still the same behavior.

/* Scala */

// Just a plain Input object which saves user's input as a state
class InputBackend($: BackendScope[Unit, String]) {
  def onChanged(e: ReactEventI): Callback ={
    $.modState((_) => {e.target.value} );
  }

  def render() = {
    <.input (
      ^.`type` := "text",
      ^.value := $.state.runNow(),
      ^.onChange ==> onChanged
    );
  }
}

// Displays a number and an <Input> next to it
class TextCounterBackend($: BackendScope[Int, Unit]) {

  ////// <--------- Moved this guy out from render()
  val inputBuilder = ReactComponentB[Unit]("Input")
    .initialState("this string wipes out any change in the input")
    .renderBackend[InputBackend]
    .build

  def render() = {
    <.div(
      <.span($.props.runNow()),
      inputBuilder(Unit)
    );
  }
}

var cnt = 0;

// Launch the counter
@JSExport
override def main(): Unit = {

    ////// <--------- Moved this guy out from setInterval()
  var textCounterBuilder = ReactComponentB[Int]("TextCounter")
    .renderBackend[TextCounterBackend]
    .build;

  setInterval(1000) {
    ReactDOM.render(
      textCounterBuilder(cnt),
      dom.document.getElementById("test_javascript_scala")
    );
    cnt += 1;
  };
}

@japgolly
Copy link
Owner

T hanks a lot
No worries 👍

Unfortunately didn't work, still the same behavior.

Try changing:

  def onChanged(e: ReactEventI): Callback ={
    $.modState((_) => {e.target.value} );
  }

to

  def onChanged(e: ReactEventI): Callback =
    $.setState(e.target.value)

@stillwaiting
Copy link
Author

Yep, this works perfectly fine. Could you please explain what was the catch?

@japgolly
Copy link
Owner

@stillwaiting It was a case of #255.

While we're here, there are a few small improvements to suggest.

You can change

  def render() = {
    <.input (
      ^.`type` := "text",
      ^.value := $.state.runNow(),
      ^.onChange ==> onChanged
    );
  }

to

  def render(state: String) =
    <.input.text(
      ^.value := state,
      ^.onChange ==> onChanged)

and

  val inputBuilder = ReactComponentB[Unit]("Input")
    .initialState("this string wipes out any change in the input")
    .renderBackend[InputBackend]
    .build

  def render() = {
    <.div(
      <.span($.props.runNow()),
      inputBuilder(Unit)
    );
  }

to

  val inputBuilder = ReactComponentB[Unit]("Input")
    .initialState("this string wipes out any change in the input")
    .renderBackend[InputBackend]
    .buildU // ← add U here because it's Unit. (This won't be needed from the next version onwards.)

  def render(props: Int) =
    <.div(
      <.span(props),
      inputBuilder())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants