Form reducer patterm

reasonreact

#1

Is there an idiomatic way to do forms? Maybe similar to ES6 Reacts style of using a computed property name with setState?

At the moment I have a reducer action for every form input which feels very boilerplatey.


#2

I’ve searched for doing this as well but it’s not typesafe and hard to do it with ReasonML. I’m using this library for dealing with form: https://github.com/Astrocoders/reform


#3

There is a new library, you might be interested in https://github.com/alexfedoseev/re-formality


#4

Hej Jasan, I’m thinking you could define your form fields using a polymorphic variant (http://2ality.com/2018/01/polymorphic-variants-reasonml.html) so that you could have a single action for updating a field (parameterised with the variant itself to ensure you’re handling all the values).

This pattern also works beautifully when mixed with result to have exhaustive error handling!

See this thread from the ocaml mailing list for some observations and approaches about how to handle exceptions (which are typically not known at compile time): https://inbox.ocaml.org/caml-list/86o9p2ywgc.fsf@gmail.com/


#5

Thanks, does sounds like it would be an improvement.

However if I’m understanding correctly, you’re just replacing having a action switch for every form item with a single action that has a switch for every form item?


#6

Edit: This has been shown to work, but pretty dangerous pattern because it’s using a possibly stale state from when the component’s reducer was last called. Don’t use it. Leaving it here as a lighthouse for anyone else who interpreted the docs as I did.

I use a single reducer that accepts a state:

reducer: (action, _state) =>
      switch action {
      | UpdateForm(newState) => ReasonReact.Update(newState)
    },

My inputs are written like:

<input 
    _type="text"
    id="someId"
    name="someName"
    className="someCSS"
    defaultValue={self.state.thisProperty}
    onChange={event => self.send(
      UpdateForm({ ...self.state, thisProperty: onChangeValue(event)})
    )}
  />

onChangeValue is:

let onChangeValue = (event) => ReactDOMRe.domElementToObj(ReactEventRe.Form.target(event))##value;

Since all the of inputs have access to a copy of self, I get the current state and can edit and pass it directly in to the reducer. All parts of the state are typed, so I can’t mess up there. I guess the risk here is side effects if people want to get risky.


#7
<input 
    _type="text"
    id="someId"
    name="someName"
    className="someCSS"
    defaultValue={self.state.thisProperty}
    onChange={event => self.send(
      UpdateForm({ ...self.state, thisProperty: onChangeValue(event)})
    )}
  />

I think using self.state inside a callback without going through self.handle (see here for more infos.) is not safe.

It’s probably best to do it that way:

let getTargetValue: ReactEventRe.Form.t => string = [%raw
  "function(event) {return event.target.value}"
];

module App = {
  module Property = {
    include
      Belt.Id.MakeComparable(
        {
          type t = [ | `firstname | `lastname];
          let cmp = Pervasives.compare;
        },
      );
    type properties = Belt.Map.t(t, string, identity);
  };
  let default =
    Belt.Map.fromArray(
      ~id=(module Property),
      [|(`lastname, "Maarek"), (`firstname, "Joseph")|],
    );
  type state = {values: Property.properties};
  type action =
    | UpdateProperty(Property.t, string);
  let component = ReasonReact.reducerComponent("App");
  let make = _children => {
    ...component,
    initialState: () => {values: default},
    reducer: (action: action, state: state) =>
      switch (action) {
      | UpdateProperty(property, value) =>
        ReasonReact.Update({
          values: state.values |> Belt.Map.set(_, property, value),
        })
      },
    render: self => {
      let getValue = property =>
        self.state.values |> Belt.Map.getWithDefault(_, property, "");
      <div className="nav-item">
        <input
          _type="text"
          id="firstname"
          name="firstname"
          value=(getValue(`firstname))
          onChange=(
            event =>
              self.send(UpdateProperty(`firstname, getTargetValue(event)))
          )
        />
        <input
          _type="text"
          id="lastname"
          name="lastname"
          value=(getValue(`lastname))
          onChange=(
            event =>
              self.send(UpdateProperty(`lastname, getTargetValue(event)))
          )
        />
      </div>;
    },
  };
};

ReactDOMRe.renderToElementWithId(<App />, "preview");

#8

Or this:

let getTargetValue: ReactEventRe.Form.t => string = [%raw
  "function(event) {return event.target.value}"
];

module App = {
  type state = {
    lastname: string,
    firstname: string,
  };
  type action =
    | UpdateLastname(string)
    | UpdateFirstname(string);
  let component = ReasonReact.reducerComponent("App");
  let make = _children => {
    ...component,
    initialState: () => {lastname: "Maarek", firstname: "Joseph"},
    reducer: (action: action, state: state) =>
      switch (action) {
      | UpdateLastname(lastname) => ReasonReact.Update({...state, lastname})
      | UpdateFirstname(firstname) =>
        ReasonReact.Update({...state, firstname})
      },
    render: self =>
      <div className="nav-item">
        <input
          _type="text"
          id="firstname"
          name="firstname"
          value=self.state.firstname
          onChange=(
            event => self.send(UpdateFirstname(getTargetValue(event)))
          )
        />
        <input
          _type="text"
          id="lastname"
          name="lastname"
          value=self.state.lastname
          onChange=(
            event => self.send(UpdateLastname(getTargetValue(event)))
          )
        />
      </div>,
  };
};

ReactDOMRe.renderToElementWithId(<App />, "preview");

#9

…is not safe

What’s unsafe about it? I’m not using self.handle because the state needs to update, the spead syntax here creates a new record with the properties of self.state with a single property being updated. A new object of type state is being passed in to the reducer, and then the state is being updated with a valid state. It’s essentially the same as | SomeAction => ReasonReact.Update({...state, someProp: someValue})


#10

What’s unsafe about it?

The answer is in the link @maarekj posted:

To access state, send and the other items in self from a callback, you need to wrap the callback in an extra layer called self.handle

If you don’t wrap it in self.handle, your code will compile—but the framework will make no guarantees that the self.state you’re spreading in the callback is the correct state (for example, you might get a stale version of it).


#11

Yea, I read the docs. It’s not all that clear when combined with the next part that talks about updating that state. It also isn’t clear because clearly you don’t need to do it in order to access those things.

If what you’re saying here is true, then you don’t need to, it’s just that the state might be stale. The documents don’t really delineate that, or point out that this is (might be) an anti-pattern.


#12

So in render you have access to the state which was correct at the time the UI was rendered. In React async mode there can be multiple updates queued before they are applied, committed to state, and re-rendered. If you rely on the stale state then on every update the code will ignore any pending updates and only write the single update based on what the state used to be.

In the reducer method you are guaranteed to have the latest state, including all updates that were queued before your own.

The try playground doesn’t use async mode so it’s hard to illustrate, but I made an example with a tight loop. Notice that when you repeat the actions only the reducer computation version composes correctly. The tight loop in this case is a good proxy for how the internals of React work in async mode.


#13

what you’re saying is definitely true, if you interpret “need” to mean “need to do this is you want to compile”. That is a sensible interpretation—much of the benefit of reasonml, and typed languages in general, is moving errors to compile time, and I’m sure “need” gets used that way elsewhere in the docs—but it is mistaken in this case. The “need” here means “if you don’t do this, your code’s correctness isn’t guaranteed”—the same way that we use the word if we said “we need to not use memory in C after it’s been freed”. The docs certainly could clarify that they mean “this will still compile, but don’t do it because it’s invalid”, but they are at least consistent given that understanding. I’m also sure, if the authors had figured out a lightweight way to enforce this via the type system, they would have preferred that to the docs route.

Also, the correct understanding of need would make this not “an anti-pattern”—that term is intended for valid use of a framework to solve a problem, that might be suboptimal in some other dimension like performance or maintainability. From the point of view of ReasonReact, not using self.handle when reading into self for a callback isn’t “not the best solution”, it’s just wrong. The specific way that it breaks when misused is an implementation detail, which they don’t want to document because it shouldn’t be relied on.