4

The question:

Is there any way to have a standard flux workflow - using Actions and Stores inside of a component and still be able to use this component for multiple different purposes, or if not is there any way to have complex nested structure in flux-react app without propagating every change trough a huge callback pipe-line?


The example (If the question is not clear enough):

Lets say I have a couple of super simple custom components like ToggleButton, Slider, DatePicker and more. They need to be reusable, so i can't use any actions inside of them, instead i've defined callback functions. For example onChange on the DatePicker fires like this:

this.props.onChange(data);

I have a custom component lets call it InfoBox that contains a couple of the simple components described above. This component listens for changes for every of its children like this:

<DatePicker ref='startDate' onChange={this.startDate_changeHandler} />

The InfoBox is used for different purposes so i guess it can not be binded to a specific store as well.

I also have a custom Grid component that render many instances of the InfoBox. This grid is used to show different data on different pages and each page can have multiple grids - so i think i can not bind it with Actions and Stores.

Now here is where it all gets crazy, bear with me - I have couple of pages - Clients, Products, Articles, etc.. each of them have at least one Grid and every grid have some filters (like search).

The pages definitely can use actions and store but there are big similarities between the pages and I don't want to have to duplicate that much code (not only methods, but markup as well).

As you may see it's quite complex structure and it seems to me that is not right to implement pipe-line of callback methods for each change in the nested components going like DataPicker > InfoBox > Grid > Page > Something else.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Chavdar Slavov
  • 865
  • 8
  • 22

1 Answers1

3

You're absolutely right in that changing the date in a DatePicker component should not trigger a Flux action. Flux actions are for changing application state, and almost never view state where view state means "input box X contains the value Z", or "the list Y is collapsed".

It's great that you're creating reusable components like Grid etc, it'll help you make the application more maintainable.

The way to handle your problem is to pass in components from the top level down to the bottom. This can either be done with child components or with simple props.

Say you have a page, which shows two Grids, one grid of - let's say - meeting appointments and one grid with todo notes. Now the page itself is too high up in the hierarchy to know when to trigger actions, and your Grid and InfoBox are too general to know which actions to trigger. You can use callbacks like you said, but that can be a bit too limited.

So you have a page, and you have an array of appointments and an array of todo items. To render that and wire it up, you might have something like this:

var TodoActions = {
  markAsComplete: function (todo) {
    alert('Completed: ' + todo.text);
  }
};

var InfoBox = React.createClass({
  render: function() {
    return (
      <div className="infobox">
        {React.createElement(this.props.component, this.props)}
      </div>
    );
  }
});

var Grid = React.createClass({
  render: function() {
    var that = this;
    return (
      <div className="grid">
        {this.props.items.map(function (item) {
          return <InfoBox component={that.props.component} item={item} />;
        })}
      </div>
    );
  }
});

var Todo = React.createClass({
  render: function() {
    var that = this;
    return (
      <div>
        Todo: {this.props.item.text}
        <button onClick={function () { TodoActions.markAsComplete(that.props.item); }}>Mark as complete</button>
      </div>
    );
  }
});

var MyPage = React.createClass({
  getInitialState: function () {
    return {
      todos: [{text: 'A todo'}]
    };
  },
  render: function() {
    return (
      <Grid items={this.state.todos} component={Todo} />
    );
  }
});

React.render(<MyPage />, document.getElementById('app'));

As you see, both Grid and InfoBox knows very little, except that some data is passed to them, and that they should render a component at the bottom which knows how to trigger an action. InfoBox also passes on all its props to Todo, which gives Todo the todo object passed to InfoBox.

So this is one way to deal with these things, but it still means that you're propagating props down from component to component. In some cases where you have deep nesting, propagating that becomes tedious and it's easy to forget to add it which breaks the components further down. For those cases, I'd recommend that you look into contexts in React, which are pretty awesome. Here's a good introduction to contexts: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html

EDIT

Update with answer to your comment. In order to generalize Todo in the example so that it doesn't know which action to call explicitly, you can wrap it in a new component that knows.

Something like this:

var Todo = React.createClass({
  render: function() {
    var that = this;
    return (
      <div>
        Todo: {this.props.item.text}
        <button onClick={function () { this.props.onCompleted(that.props.item); }}>Mark as complete</button>
      </div>
    );
  }
});

var AppointmentTodo = React.createClass({
  render: function() {
    return <Todo {...this.props} onCompleted={function (todo) { TodoActions.markAsComplete(todo); }} />;
  }
});

var MyPage = React.createClass({
  getInitialState: function () {
    return {
      todos: [{text: 'A todo'}]
    };
  },
  render: function() {
    return (
      <Grid items={this.state.todos} component={AppointmentTodo} />
    );
  }
});

So instead of having MyPage pass Todo to Grid, it now passes AppointmentTodo which only acts as a wrapper component that knows about a specific action, freeing Todo to only care about rendering it. This is a very common pattern in React, where you have components that just delegate the rendering to another component, and passes in props to it.

Anders Ekdahl
  • 22,685
  • 4
  • 70
  • 59
  • Thank you for your answer, however before accepting it i have a minor request - can you please include in your example how the `Todo` component can be used to handle checking the *appointments* as well as the *Todos*, I'm not sure do you need to pass the action as a property or is there any better way ? – Chavdar Slavov Apr 30 '15 at 09:07
  • I'm not sure I understand. In my example, todos and appointments were two separate things that didn't have anything to do with each other. That's also why I omitted appointments from the example, because you'd just duplicate the approach I gave with todos. Could you elaborate on what you'd like me to show? What does "checking the appointment" mean in the context of a todo? – Anders Ekdahl Apr 30 '15 at 10:27
  • Lets say that the *appointments* are also checklist exactly like the *todo* list, they just use the data from a different store. I need to be able to reuse the `Todo` class in this case. – Chavdar Slavov Apr 30 '15 at 11:00
  • I've updated my answer. Hope it answers your question. – Anders Ekdahl Apr 30 '15 at 11:10
  • I was hoping for more flexible solution, but I realize that there might not be one, thank you for your time. – Chavdar Slavov Apr 30 '15 at 11:29
  • Well, my approach here is just one way of solving it. And it's hard to help you out with a better approach when I don't know more about the problem you're trying to solve. But since React really embraces Javascript and composition, you can use all the strengths of Javascript to make a robust and flexible solution. You can extract common things into small modules, you can use React contexts, you can pass props, etc. I think you might like Pete Hunts clever use of contexts as a sort of dependency injection: http://jsfiddle.net/cjL6h/ – Anders Ekdahl Apr 30 '15 at 11:33