Rescript-react beginner confusion, type-safety

I’m having trouble figuring out how useReducer and React updates work in general. I’ve made a minimal example. First, a place to store the data that I want to share among various components:

// Store.res

type state = {
  thickness: int,
}

let initialState: state = {
  thickness: 10,
}

type action =
  | ChangeThickness({thickness: int})
  | NoOp

let updateThickness: (state, int) => state = (s:state, t: int) => {...s, thickness: t};

let reducer = (state: state, action: action) => {
  switch action {
  | ChangeThickness({thickness}) => updateThickness(state, thickness)
  | NoOp => state
  }
}

Second, a component that uses this:

//Controls.res
@react.component
let make = (~state: Store.state, ~dispatch: Store.action => unit) => {
  <main>
    <form>
      <label htmlFor="thickness"> {React.string("Thickness ")} </label>
      <input
        type_="number" id="thickness" min="0" max="10"
        value={Js.Int.toString(state.thickness)}
        onChange={event => { 
          dispatch(
            Store.ChangeThickness({
              thickness: int_of_string(ReactEvent.Form.target(event)["value"]),
            }),
          )
        }} 
      />
      <br />
      <span className="foo"> {state.thickness->string_of_int->React.string} </span>
      <br />
      <button
        onClick={_ => {
          dispatch(Store.NoOp)
        }}>
        {React.string("Refresh")}
      </button>
    </form>
  </main>
}

and third, an App that loads those controls:

//App.res
@react.component
let make = () => {
  let (state, dispatch) = React.useReducer(Store.reducer, Store.initialState);
  (<div><Controls state dispatch/></div>);
} 

Finally, the html file, index.html:

<!DOCTYPE html>
<html lang="en">
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

The result? When I click the up-and-down arrows on the “thickness” field, the displayed value goes up and down; that part’s great. Suppose I click down until it’s, say, 6. If I then click on the Refresh button, everything goes back to 10 (!).

While trying to create this minimal example of behaviour I wasn’t expecting, I tried removing the <form>...</form> pair. in Controls.res. When I do that…the behavior is what I’d expect (namely, the “6” remains “6”).

Can someone explain?

On a rather different (and more ReScript-y) note, if in Controls.res I change the line

thickness: int_of_string(ReactEvent.Form.target(event)["value"]),

to

thickness: ReactEvent.Form.target(event)["value"],

I don’t get any actual type-error, although I expected one. Looking at the javascript code executing, I see that the thickness value changes from 10 to "9" and so on, remaining a string as I keep clicking. This seems very weird to me.

Can someone explain this? It seems like a real violation of type-safety.

I’ve partly answered my own question at this point – the unexpected behavior comes from the semantics of form-submission (and the implicit assumption that any button inside a form must actually be a “submit” button on top of that!). Deleting the form tag resolves this particular problem. But the type-error problem is more troubling to me, and remains open.

… the unexpected behavior comes from the semantics of form-submission (and the implicit assumption that any button inside a form must actually be a “submit” button on top of that!). Deleting the form tag resolves this particular problem.

Alternatively, you could explicitly set type as button, for example:

<form>
  ...
  <button type_="button">{React.string("Refresh")}</button>
</form>

… But the type-error problem is more troubling to me, and remains open.

The return type of ReactEvent.Form.target(e) is {..}, because {..} is an open JS object type, the type of the ReactEvent.Form.target(e)["value"] will be inferred by the compiler. Let’s compare these two:

  1. thickness: int_of_string(ReactEvent.Form.target(event)["value"])
    → The type of the ReactEvent.Form.target(event)["value"] will be inferred as string and then you convert (int_of_string) it to an int.

  2. thickness: ReactEvent.Form.target(event)["value"]
    → The type of ReactEvent.Form.target(event)["value"] will be inferred as int.

In this case, the type of the ReactEvent.Form.target(event)["value"] should be string.

If you go for thickness: ReactEvent.Form.target(event)["value"], it seems work in the beginning when you just read the value and display it, but if you add some calculation like this:

<span className="foo"> {(state.thickness + 1)->string_of_int->React.string} </span>

You’d be surprised by the result.

I think my question arose because I already was surprised by the result!

When you say that in case 2, "The type of ReactEvent.Form.target(event)["value"] will be inferred as int ", do you mean that the compiler will say “I don’t know what the type of this javascript value actually is, but he’s assigning it to an int, so … I’m going to pretend the return type is int, and let him make the assignment. That’ll compile into javascript in which a string is assigned to the variable “thickness”, and he’ll just have to debug it,” or something like that?

This seems roughly to be "don’t expect type-safety when you’re interacting with React", but maybe I’m misunderstanding.