Skip to content
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

Recommended way to implement forms #145

Closed
ivan-kleshnin opened this issue Mar 24, 2015 · 15 comments
Closed

Recommended way to implement forms #145

ivan-kleshnin opened this issue Mar 24, 2015 · 15 comments
Labels

Comments

@ivan-kleshnin
Copy link
Contributor

When I did Reflux-based app I just used this.state.model as both initial data for forms
and write target at form changes. It was just a pointer to the same data for both component and store.
so it worked and was more convenient than piping every field through actions (omg).

With Baobab such trick is impossible(?), as data is "immutable". I wonder how people implement their forms and what are recommended approaches. 2-way data flow is very handy with forms so I'd like to have it in some kind.

@marbemac
Copy link

Great question - I'm just running into this now and will share what I find. Curious to hear other people's solutions. @christianalfoni? @Yomguithereal?

@ivan-kleshnin
Copy link
Contributor Author

@marbemac, I see a few possible directions:

  1. Reflect new / edited model state in Baobab. Update state through the actions as usual.

  2. Reflect new / edited model state in Baobab. Update Baobab state right from the component. No Action involved in field editing.

Quoting @Yomguithereal:

// This is not necessarily a good idea to let your components handle the state updates by
// themselves but you could do it if needed.
this.cursors.name.edit('John');

  1. Use Baobab as default data for edited model only. Mirror new / edited model state in Form Component. Update Baobab state on submit only. No Action or State involved in field editing.

So:

  1. – big loop Component => Action => State => Component

  2. – medium loop Component => State => Component

  3. – inner loop Component [=> Action => State on submit]

  4. looks like easiest and shortest one. But there are cases when you'll want to go tedious 1)
    E.g. when you want to spy track visitor actions (wrong inputs etc.). Not sure about 2)

@christianalfoni wrote a good article about forms, but there are no mention of Baobab (article is old enough and focus differs a bit). He desribed approach 3)

@ivan-kleshnin
Copy link
Contributor Author

There are two types of forms: with immediate save and with save on submit. First type of forms is popular in OS settings (immediate reaction, not required to scroll long list of options...) but is not really applicable for creation. immediate forms have no clue when they should submit data for new model in general case. So CRUD should be either implemented with a mix of the two (complex) or with second type only (easy). That's why I propose to ignore the possibility of immediate forms for a while in this issue.

@Yomguithereal
Copy link
Owner

The other thing here is to be able to define whether your Baobab tree, i.e. your app's state, really needs to hold the state of your form, or at least to be in full sync when the inputs change. In most cases, you don't and state component is way simpler and can handle the form alone. In other, however, for whatever reasons, you'll need to store the form's state into the app's one. Only one tricky thing here with React: you'll have to commit the state changes synchronously if you don't want to observe nasty cursor hopping.

this.tree.commit();

@ivan-kleshnin
Copy link
Contributor Author

@Yomguithereal I agree with what you say about Baobab aspect of this.

But the most complex thing here is how to request external data from component. Every example in the Internet just shows data already preloaded, lol. There is no way to declare external data dependency in component, React has nothing to help your: hooks doesn't allow to return promises, etc.

I'm getting very complex and error prone code when trying to emulate that in imperative way.

getInitialState - shouldn't contain async code
componentWillUpdate - doesn't support immutable data, isn't called at first rendering
componentWillMount - doesn't support promise return
componentDidMount - shouldn't change state

So we'll end up with at least few rerender sessions for form component.
With tricky manual state management of loaded / loadError crap.

React is shockingly helpless with data loading questions.

@ivan-kleshnin
Copy link
Contributor Author

Well, I've just decided to accept reality, and simply preload all data from the start.
Otherwise React is plainly unusable as all it's API is not composable with async stuff.
Practicality beats purity, yes

@Yomguithereal
Copy link
Owner

@ivan-kleshnin, what about cycleJS?

@ivan-kleshnin
Copy link
Contributor Author

@Yomguithereal unfortunately, I don't reach form question there. I continue to research both React and Cycle ecosystem, I try to elaborate proof-of-concept apps in both. CycleJS is a long-distance goal for me as it will be compatible with Web Components and have a lot of other benefits (at least in theory) but the lack of matherial about RxJS, the lack of examples slows everything.

One of the big problems with React (as I've already said a number of times) is it's hooks.
Behavior of hooks is predefined and can't be customized. Hooks are limited and limiting.
They are too class-oriented. In CycleJS you won't have any as you operate with simplier abstractions.

React just merges rendering, control and state aspects in Component. Seals it and expose hooks.
At the App level you operate with different primitives. CycleJS solves this by providing the same usable primitives (Model, View, Intent) at all levels. That's the main architectural benefit besides all that Rx stuff.

As long as I will have some form examples on Cycle, I'll drop them here.

@ivan-kleshnin
Copy link
Contributor Author

I've got working edit form prototype but with huge hacks. Please review and advice how to improve.
As I've said data hydration is the most tricky part. Reflux broadcasted state at component connection, at least. Baobab somehow misses this point...

// app.js
RobotActions.loadMany(); // trigger inital preload of data from global space
// state.js
export default new Baobab({
  robots: {
    models: {}, // format is {[id]: {id: ..., name: ...}} 
    loaded: false,
    loadError: undefined,
  }
});
// EditRobotComponent.js
export default React.createClass({
  mixins: [State.mixin],

  cursors() {
    return {
      robots: ["robots"], // required to track load status
      loadModel: ["robots", "models", this.getParams().id], // "reference" model (see reset)
    }
  },

  getInitialState() {
    return {
      model: undefined, // mutable model, reflects state of fields
      loaded: false,
      loadError: undefined,
      loadModel: { // "immutable" reference model
        name: undefined,
        assemblyDate: undefined,
        manufacturer: undefined,
      },
    }
  },

  componentWillMount() {
    // `componentWillUpdate` is not called at first render, and data is not "changed"
    // so we need to hack this manually. How to handle this case without hack???
    RobotActions.askData();
  },

  componentWillUpdate(nextProps, nextState) {
    // clone "immutable" referential instance into working instance
    if (!this.state.model && nextState.cursors.loadModel) {
      nextState.model = Object.assign({}, nextState.cursors.loadModel);
    }
  },

  render() {
    let {models, loaded, loadError} = this.state.cursors.robots;
    let loadModel = this.state.cursors.loadModel
    let model = this.state.model;

    if (!loaded) {
      return <Loading/>;
    } else if (loadError) {
      return <NotFound/>;
    } else {
      return (
        <DocumentTitle title={"Edit " + model.name}>
          ... form ...
        </DocumentTitle>
      );
    }
  },
}
// RobotActions.js
export function askData() {
  State.select("robots").set("timestamp", new Date()); // dirty hack to trigger data sharing
}

Playground: https://github.com/Paqmind/react-starter#baobab

@ivan-kleshnin
Copy link
Contributor Author

React mixins are completely broken. No wonder they deprecate it.

Follow me: Baobab State.mixin provides hook getInitialState() which yields all data I need.
The task is to compute just one additional state field from it.

I have second implementation of the same getInitialState() in Component. It's get called after mixin's but it does not receive any data in arguments as it logically should 😞
It does not have access to this.state as well.

So the only way to solve this case is to split component into "data-source component" and "working component". Everyone seems to hate mixins now and for a reason. But the root of the problem lies in React's inability to provide decent compositional blocks. Components are extremely overcomplicated.

They complect state, rendering, actions, deps from external data and DOM side-effects.
This is God-object anti-pattern in it's best fit.

Happy hipsters recommend just what I said. Extract data-source aspects from components and put them... into different components. Flummox does this and Reflux follows. But then we combine them... in HTML topology. This is nothing better than Angular attempts to stretch HTML language to have programming language constructs.

It's obviously broken for multi-step dependencies. Every dependency step will require additional wrapper. Or it's better to surrender and put data in State, because there layering will be assembled from normal PL primitives, not HTML. But for forms it's inconvenient and our first requirement was to keep everything inside...

All described is required for Edit forms only. For Add forms there is no Baobab reference model.
So we either implement them differently or add worthless proxy component to Add form to keep symmetry.

Working with React every business-case feels like an edge-case.

Forms? CRUD? Data load? Data broadcast?
That's soo specific 😠

@marbemac
Copy link

Using my query mixin, this became fairly trivial.

https://gist.github.com/marbemac/a18582da8d2912285950#file-mixins-js

var Post = React.createClass({
  mixins: [ StateMixins.query ],

  queries: {
    post: {
      cursors: {
        entries: ['models', 'posts'],
      },
      local: function(_this, data) {
        // using react-router
        var id = _this.context.router.getCurrentParams().postId;
        // if we're editing a post, fetch it
        if (id) {
          return StateQuery.findOne(data.posts.get(), {id: id});
        } else { // not editing a post? return a new one right away
          return new PostModel();
        }
      },
      remote: function(_this, data) {
        return StateActions.findOne(
          StateActions.models.POST, 
          _this.context.router.getCurrentParams().postId,
          false
        );
      }
    }
  }
})

In the local function, if there is a postId in the url, we look for the post locally (if not found, it will call the remote function and fetch it via the api). If no postId, then we just immediately return a new Post. Then, in render and all the functions that are called on form input updates, I use/edit this.state.post.result directly.

@ivan-kleshnin
Copy link
Contributor Author

@marbemac I think that's too much for one component. Tha'ts exactly what is called God-object.

@marbemac
Copy link

@ivan-kleshnin Why is it too much for one component? I understand the concept of the god-object, and don't believe this concept applies to components built in the way described above.

From the wikipedia god-object definition:

"Most of such a program's overall functionality is coded into a single "all-knowing" object, which maintains most of the information about the entire program..."

The react component described in my post knows nothing about the rest of the program. It knows enough to render itself, and that's it. It is not a god-object.

I actually modeled this after Relay. In relay, each component contains a definition defining the data needs for that component. This is useful, because now a component is self sufficient, and depends only on itself. You can drop it in anywhere and it will just work. It knows nothing about the rest of the system, or other components - it just knows how to render itself.

I'm curious about how you are fetching/managing data for your components? I'm totally open to other solutions! For me, so far, the solution above has been working quite well.

@lucasmreis
Copy link

I'm updating Baobab tree on onBlur event:

import State from '../state';

var EmailForm = React.createClass({
  mixins: [State.mixin],

  cursors: {
    email: ['email']
  },

  updateEmail: function(v) { 
    State.set('email', v);
  },

  render: function() {
    return <input 
      defaultValue={ this.cursors.email.get() }
      onBlur={ this.updateEmail } />
  }
});

I would put it on onChange too, but the cursor get kind of crazy. Anyone has a better solution to it?

Back to the code, instead of calling State.set inside the React class, I would put it in another module, then it could transform or validate before updating or not the tree.

That way we would have what could be called an ASV architecture: Actions > State > View. Does it make sense?

@Yomguithereal
Copy link
Owner

@lucasmreis, you probably have cursor issues because baoab's updates are asynchronous and because react cannot handle controlled input with asynchronous state updates. You should try tree.commit to apply tree state changes synchronously in this case.

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

4 participants