-Поиск по дневнику

Поиск сообщений в rss_planet_mozilla

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 19.06.2007
Записей:
Комментариев:
Написано: 7


Pomax: A look at thoughts that have come from working with React at the [Mozilla Foundation](https://www.mozilla.org/foundation), where we use it extensively for our [Webmaker Android app](https://github.com/mozilla/webmaker-android/)

Среда, 10 Июня 2015 г. 01:21 + в цитатник

First off, the single most important note about React I ever wrote.

Let me start this blog post with the most important thing about React, so that we understand why things happen the way they do:

If used correctly, your users will think they are manipulating a UI, when in fact they are manipulating React, which may then update the UI

Sounds simple, doesn't it? But it has a profound effect on how UI interactions work, and how you should be thinking about data flow in React applications. For instance, let's say we have a properly written React application that consists of a page with some text, and a slider for changing the text's opacity. I move the slider. What happens?

In traditional HTML, I move the slider, a change event is triggered, and if I had an eventListener hooked up for that, I could then do things based on that change.

React doesn't work like that

Another short and sweet sentence: React doesn't work with "changes to the UI" as you make them; React doesn't allow changes to the UI without its consent. Instead, React intercepts changes to the UI so they don't happen, then triggers the components that are tied to the UI the user thinks they're interacting with, so that those components can decide whether or not a UI update is necessary.

In a well written React application, this happens:

  1. I try to move the slider.
  2. The event is intercepted by React and killed off.
  3. As far as the browser knows, nothing has happened to that slider.
  4. React then takes the information about my UI interaction, and sends it to the component that owns the slider I tried to manipulate.
  5. If that component accepts my attempt at changing the UI, it will update its state such that it now renders in a way that makes it look identical to the traditional HTML case:
  6. As far as I'm concerned, as a user, I just moved the slider. Except in reality I didn't, my UI interaction asked React to have that interaction processed and that processing caused a UI update.

This is so different from traditional HTML that you're going to forget that. And every time you do, things will feel weird, and bugs might even be born. So, just to hopefully at least address that a tiny bit, once more:

If used correctly, your users will think they are manipulating a UI, when in fact they are manipulating React, which may then update the UI

Now then, try to remember this forever (I know, simple request, right?) and let's move on.

Revisiting the core concepts of modeling "your stuff" with React

If you've been working with React for a while it's easy to forget where "your mental construct of a thing" ends and where your UI components begin, and that makes it hard to reason about when to use React's state, when to use props, when to use instance variables, and when to offload things entirely to imported functionality objects. So, a quick refresher on the various bits that we're going to be looking at, and how to make use of them:

Your thing

This is an abstract idea, and generally breaks up into lots of tiny things that all need to "do something" to combine into a larger whole that actual humans like to think in. "A blog post", "A page", or "a markdown editor" all fall into this category. When thinking about "your thing", it's tempting to call the specific instantiation of everything that this thing needs "its state", but I'm going to have to be curt and tell you to not do that. At least, let's be specific: whenever we talk about the thing's state, let's call it "the full state". That way we won't get confused later. If it doesn't have "full" in the description, it's not your thing's abstract meta all encompassing state.

React components

These are extremely concrete things, representing UI elements that your users will interact with. Components need not map one-to-one to those abstract ideas you have in your head. Think of components as things with three levels of data: properties, state, and "plain old javascript stuffs".

Component properties: "this.props"

These are "constructor" properties, and are dictated by whoever creates an instance of the component. However, React is pretty clever and can deal with some situations in ways you may not expect coming at React from a traditional HTML programming paradigm. Let's say we have the following React XML - also known as JSX (This isn't HTML or even XML, it's just a more convenient way to write out programming intent, and maps directly to a React.createElement call. You can write React code without ever using JSX, and JSX is always first transformed back to plain JS before React runs it. Which is why you can get JavaScript errors "in your XML", which makes no sense if you still think that stuff you wrote really is XML):

Parent = React.createClass({
  render() {
    return (
<...> <...>
); } });

The child's position is always the same in this case, and when the Parent first renders, it will create this Child with some content. But this isn't what really happens. React actually adds a level of indirection between the code you wrote, and the stuff you see client-side (e.g. the browser, a native device, etc.): a VIRTUAL DOM has been created based on your JSX, and it is that VIRTUAL DOM that actually controls how things are changed client-side. Not your code. So the diffrence kicks in when we change the content that should be in that child and we rerender, to effect a new child:

  • Something happens to Parent that changes the output of getCurrentChildContent
  • Parent renders itself, which means the has changed.
  • React updates the VIRTUAL element associated with the Parent, and one of the updates is for the VIRTUAL Child element, which has a new property value
  • Rather than destroying the old Child and building a new one with the new property, React simply updates the VIRTUAL element so that it is indistinguishable from what things would have been had we destroyed and created anew.
  • the VIRTUAL DOM, once marked as fully updated, then reflects itself onto client the so that users see an updated UI.

The idea is that React is supposed to do this so fast you can't tell. And the reason React is so popular is that it actually does. React is fast. Really fast.

Where in traditional HTML you might remove(old) and then append(new), React will always, ALWAYS, first try to apply a "difference patch", so that it doesn't need to waste time on expensive construction and garbage collection. That makes React super fast, but also means you need to think of your components as "I am supplying a structure, and that structure will get updated" instead of "I am writing HTML elements". You're not.

Component state: "this.state"

This is the state of the React component. A react component that represents a piece of interactive text, for instance, will have that text bound as its state, because that state can be changed by the component itself. Components do not control what's in their props (beyond the limited 'use these default values for props that were not passed along during construction'), but they do control their state, and every update to the state triggers a render() call.

This can have some interesting side effects, and requires some extra thinking: If you have a text element, and you type to change that text, that change needs to be reflected to the state before it will actually happen.

Remember that important sentence from the start of the post:

If used correctly, your users will think they are manipulating a UI, when in fact they are manipulating React, which may then update the UI

And then let's look at what happens:

  • the user types a letter in what they think is a text input field of some sort
  • the event gets sent to React, which kills it off immediately so the browser never deals with it, and then sends it on to the component belonging to the VIRTUAL element that backs the UI that the user interacted with
  • the component handles the event by extracting the data and updating its state so that its text reflects the new text
  • the component renders itself, which updates the VIRTUAL element that backs the UI that the user sees, replacing its old text (pre-user-input) with the next text (what-the-user-thinks-they-wrote). This change is then reflected to the UI.
  • the user sees the updated content, and all of this happened so fast that they never even notice that all this happens behind the scenes. As far as they know, they simply typed a letter.

If we didn't use this state reflecting, instead this would happen:

  • user types a letter
  • React kills off the event to the VIRTUAL element
  • there is no handler to accept the event, extract its value, and update the component state, so:
  • nothing happens.

The user keeps hitting the keyboard, but no text shows up, because nothing changes in React, and so nothing changes in the UI. As such, state is extremely important to get right, and remembering how React works is of crucial importance.

Semantically refactored state: mixins

In additional to properties and state, React has a "mixin" concept, which allows you to write utility code that can tack into/onto any React class you're working with. For instance, let's look at an input component:

var Thing = React.createClass({
  getInitialState: function() {
    return { input: this.props.input || "" };
  },
  render: function() {
    return 
  },
  updateInput: function(evt) {
    this.setState({ input: evt.target.value }, function() {
      if (this.props.onUpdate) {
        this.props.onUpdate(this.state.input);
      }
    });
  }
});

Perfectly adequate, but if we have lots of components that all need to work with inputs, we can also do this:

var inputMixin = {
  getInitialState: function() {
    return {
      input: this.props.input || ""
    };
  },
  updateInput: function(evt) {
    this.setState({ input: evt.target.value }, function() {
      if (this.props.onUpdate) {
        this.props.onUpdate(this.state.input);
      }
    });
  }
};

var Thing = React.createClass({
  mixins: [ inputMixin ],
  render: function() {
    return 
  },
});

We've delegated the notion of input state tracking and UI handling to a "plain JavaScript" object. But, one that hooks into React's lifecycle functions, so even though we define the state variable input in the mixin, the component will end up owning it and this.state.input anywhere in its code will resolve just fine.

Mixins allow you to, effectively, organise state and behaviour in a finer-grained way than just components allow. Multiple components that have nothing in common with respects to your abstract model can be very efficiently implemented by looking at which purely UI bits they share, and modeling those with single mixins. Less repetition, smaller components, better control.

Of course, it gets tricky if you refer to a state variable that a mixin introduces outside of that mixin, so that's a pitfall: ideally, mixins capture "everything" so that your components don't need to know they can do certain things, "they just work". As such, I like to rewrite the previous code to the following, for instance:

var inputMixin = {
  getInitialState: function() {
    return {
      input: this.props.input || ""
    };
  },
  updateInput: function(evt) {
    this.setState({ input: evt.target.value }, function() {
      if (this.props.onUpdate) {
        this.props.onUpdate(this.state.input);
      }
    });
  },
  // JSX generator function, so components using this mixin don't need to
  // know anything about the mixin "internals".
  generateInputJSX: function() {
    return 
  }
};

var Thing = React.createClass({
  mixins: [ inputMixin ],
  render: function() {
    return (
      
... { this.generateInputJSX() } ...
); }, });

Now the mixin controls all the things it needs to, and the component simply relies on the fact that if it's loaded a mixing somethingsometingMixin, it can render whatever that mixin introduces in terms of JSX with a call to the generateSomethingsomethingJSX function, which will do the right thing. If the state for this component needs to be saved, saving this.state will include everything that was relevant to the component and the mixin, and loading the state in from somewhere with a setState(stateFromSomewhere()) call will also do the right thing.

So now we can have two completely different components, such as a "Portfolio" component and a "User Signup" component, which have absolutely nothing to do with each other, except that they will both need the UI and functionality that the inputMixin can provide.

(Note that while it is tempting to use Mixins for everything, there is a very simple criterium for whether or not to model something using mixins: does it rely on hooking into React class/lifecycle functions like getInitialState, componentDidUpdate, componentWillUnmount, etc.? If not, don't use a mixin. If you just want to put common functions in a mixin, don't. Just use a library import, that's what they're for)

Instance variables and externals

These things are handy for supporting the component, but as far as React is concerned they "don't matter", because updates to them do nothing for the UI unless there is extra code for manually triggering a state change. And you can't trigger a state change on an instance variable, state changes happen through setState and property updates by parents.

That said, React components are just plain JavaScript, so there is nothing preventing you from using the same JS constructs that we use outside of React:

var library = require("libname");
var Thing = React.createClass({
  mixins: [
    require("somemixin"),
    require("someothermixin")
  ],
  getInitialState: function() {
    this.elements = library.getStandardList();
    return { elements: this.elements };
  },
  addElement: function(e) {
    this.elements.push(e);
    this.setState({ elements: this.elements });
  },   
  render: function() {
    return this.state.elements.map(...);
  }
});

Perfect: in fact, using instance variables sometimes drastically increases legibility and ease of development, such as in this example. Calling addElement() several times in rapid succession, without this.elements, has the potential to lose state updates, effectively doing this:

  1. var l1 = this.state.elements; + l1.push(e) + setState({ elements: l1 });
  2. var l2 = this.state.elements; + l2.push(e) + setState({ elements: l2 });
  3. var l3 = this.state.elements; + l3.push(e) + setState({ elements: l3 });

Now, if l3 is created before setState for l2 has finished, then l3 is going to be identical to l1, and after it's set, l2 could be drop over it, losing us data twice!

Instance variables to the rescue.

Static properties on the component class

Finally, components can also be defined with a set of static properties, meaning they exist "on the class", not on specific instances:

var Thing = React.createClass({
  statics: [
    mimetypes: require("mimetypes")
  ],
  render() {
    return 
I am a { this.props.type }!
; } }); var OtherThing = React.createClass({ render: function() { } });

Of course like all good JS, statics can be any legal JS reference, not just primitives, so they can be objects or functions and things will work quite well.

Back to React: hooking up components

The actual point of this blog post, in addition to the opener sentence, was to look at how components can be hooked up, by choosing how to a) model state ownership, b) model component interactions, and c) data propagation from one component to another.

This is going to be lengthy (but hopefully worth it) so let's just do this the itemized list way and work our way through. We have two lists:

State ownership:

  1. centralized ownership
  2. delegated ownership
  3. fragmented ownership
  4. black box ownership

Component interactions:

  1. Parent to Child
  2. Parent to Descendant
  3. Child to Parent
  4. Child to Ancestor
  5. Sibling to Sibling

Data propagation:

  1. this.props chains
  2. targeted events using publish/subscribe
  3. blind events broadcasting

So I'm going to run through these, and then hopefully at the end tie things back together by looking at which of these things work best, and why I think that is the case (with which you are fully allowed to disagree and we should talk! Talking is super useful).

Deciding on State Ownership

Centralized ownership

The model that fits the traditional HTML programming model best is the centralized approach, where one thing "owns" all the data, and all changes go through it. In our editor app, we can model this as one master component, "Parent", with two child components, "Post" and "Editor", which take care of simply showing the post, and editing the post, respectively.

Out post will consist of:

var marked = require("marked");
var Post = React.createClass({
  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: marked(this.props.content);
      }
    };
    return 
; } });

Our editor will consist of:

var tinyMCE = require("tinymce");
var Editor = React.createClass({
  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: tinymce({
          content: this.props.content,
          updateHandler: this.onUpdate
        });
      }
    };
    return 
; }, onUpdate: function(evt) { this.props.onUpdate(evt); } });

And our parent component will wrap these two as:

var Parent = React.createClass({
  getInitialState: function() {
    return {
      content: "",
      editing: false
    };
  },

  render: function() {
    return (
); }, // triggered when we click the post switchToEditor: function() { this.setState({ editing: true }); }, // Called by the editor component onUpdate: function(evt) { this.setState({ content: evt.updatedContent, editing: false }); } });

In this setup, the Parent is the lord and master, and any changes to the content must run through it. Saving and loading of the post to and from a data repository would, logically, happen in this Parent class. When a user clicks on the post, the "hidden" flag is toggled, which causes the Parent to render with the Editor loaded instead of the Post, and the user can modify the content to their heart's content. Upon completion, the Editor uses the API that the Parent passed down to ensure that its latest data gets reflected, and we return to the Post view.

The important question is "where do we put save and load", and in this case that choice is obvious: in Parent.

var staterecorder = {
  componentWillMount: function() {
    this.register(this, function loadState(state) {
      this.setState(state);
    });
  },

  register

  componentWillUnmount: function() {
    this.unregister(this);
  },
}

var Parent = React.createClass({
  mixins: [
    require("staterecorder")
  ]

  getInitialState: function() {
    ...
  },

  getDefaultProps: function() {
    return { id: 1};
  },

  render: function() {
    ...
  },

  ...
});

But: why would the Parent be in control? While this design mirrors our "abstract idea", this is certainly not the only way we can model things. And look closely: why would that Post not be the authoritative source for the actual post? After all, that's what we called it. Let's have a look at how we could model the idea of "a Post" by acknowledging that our UI should simply "show the right thing", not necessary map 1-on-1 to our abstract idea.

Delegated state management

In the delegated approach, each component controls what it controls. No more, no less, and this changes things a little. Let's look at our new component layout:

Out post is almost the same, except it now controls the content, and as such, this is now its state and it has an API function for updating the content if a user makes an edit (somehow) outside of the Post:

var marked = require("marked");
var database = require("database");
var Post = React.createClass({
  getInitialState: function() {
    content: ""
  },

  componentWillMount: function() {
    database.getPostFor({id : this.props.id}, function(result) {
      this.setState({ content: result });
    };
  },

  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: marked(this.props.content);
      }
    };
    return 
; }, setContent: function(newContent) { this.setState({ content: newContent }); } });

Our editor is still the same, and it will do pretty much what it did before:

var tinyMCE = require("tinymce");
var Editor = React.createClass({
  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: tinymce({
          content: this.props.content,
          updateHandler: this.onUpdate
        });
      }
    };
    return 
; }, onUpdate: function(evt) { this.props.onUpdate(evt); } });

And our parent, however, has rather changed. It no longer controls the content, it is simply a convenient construct that marries the authoritative component, with some id, to an editor when the user needs it:

var Parent = React.createClass({
  getInitialState: function() {
    return {
      editing: false
    };
  },

  render: function() {
    return (
); }, // triggered when we click the post switchToEditor: function() { ?????? this.setState({ editing: true }); }, // Called by the editor component onUpdate: function(evt) { this.setState({ editing: false }, function() { this.refs.setContent(evt.newContent); }); } });

You may have spotted the question marks: how do we now make sure that when we click the post, we get its content loaded into the editor? There is no convenient "this.props" binding that we can exploit, so how do we make sure we don't duplicate things all over the place? For instance, the following would work, but it would also be a little ridiculous:

var Parent = React.createClass({
  getInitialState: function() {
    return {
      editing: false,
      localContent: ""
    };
  },

  render: function() {
    return (
); }, bindContent: function(newContent) { this.setSTate({ localContent: newContrent }); }, // triggered when we click the post switchToEditor: function() { this.setState({ editing: true }); }, // Called by the editor component onUpdate: function(evt) { this.setState({ editing: false }, function() { this.refs.post.setContent(evt.newContent); }); } });

We've basically turned the Parent into a surrogate Post now, again with its own content state variables, even though the set out to eliminate that. This is not a path to success. We could try to circumvent this by linking the Post to the Editor directly in the function handlers:

var Parent = React.createClass({
  getInitialState: function() {
    return {
      editing: false
    };
  },

  render: function() {
    return (
); }, // triggered when we click the post switchToEditor: function() { this.refs.editor.setContent(this.refs.post.getContent(), function() { this.setState({ editing: true }); }); }, // Called by the editor component onUpdate: function(evt) { this.setState({ editing: false }, function() { this.refs.post.setContent(evt.newContent); }); } });

This might seem better, but we've certainly not made the code easier to read by putting in all those async interruptions...

Fragmenting state across the UI

What if we took the genuinely distributed approach? What if we don't have "a Parent", with the Post and Editor being, structurally, sibling elements? This would certainly rule out the notion of duplicated state, but also introduces the issue of "how do we get data from the editor into the post":

var Post = React.createClass({
  getInitialState: function() {
    content: ""
  },

  componentWillMount: function() {
    database.getPostFor({id : this.props.id}, function(result) {
      this.setState({ content: result });
    };

    somewhere.listenFor("editor:update", this.setContent);
  },

  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: marked(this.props.content);
      },
      onClick: this.onClick
    };
    return 
; }, onClick: function() { // somehow get an editor, somewhere, to open... somewhere.trigger("post:edit", { content: this.state.content }); }, setContent: function(newContent) { this.setState({ content: newContent }); } });

The obvious thing to notice is that the post now needs to somehow be able to trigger "an editor", as well as listen for updates.

var Editor = React.createClass({
  componentWillMount: function() {
    somewhere.listenFor("post:edit", function(evt) {
      this.contentString = evt.content;
    });
  },

  render: function() {
    var innerHTML = {
      dangerouslySetInnerHTML: {
        __html: tinymce({
          content: this.contentString,
          updateHandler: this.onUpdate
        });
      }
    };
    return 
; }, onUpdate: function(evt) { somewhere.trigger("editor:update", evt); } });

Again, this seems less than ideal. While the Post and Editor are now nice models, we're spending an aweful lot of time in magical-async-event-land, and as designers, programmers, and contributors, we basically have no idea what's going on without code diving.

Remember, you're not just writing code for you, you're also writing code for people you haven't met yet. We want to make sure we can onboard them without going "here are the design documents and flowcharts, if you see anything you don't understand, please reach out and good luck". We want to go "here's the code. It's pretty immediately obvious how everything works, just hit F3 in your code editor to follow the function calls".

Delegating all state to an external black box

There is one last thing we can try: delegating all state synchronizing to some black box object that "knows how to state, yo". For instance, a database interfacing thing through which we perform lookups and save/load all state changes. Of all the options we have, this is the one that is absolutely the most distributed, but it also comes with some significant drawbacks.

var api = {
  save: function(component, state) {
    // update our data store, and once that succeeds, update the component
    datastore.update(component, state).success(function() {
      component.setState(state);
    });
  }
};

var Post = React.createClass({
  ...
  componentWillMount: function() {
    api.load(this, function(state) {
      this.setState(state);
    });
  },
  setContent: function(newContent) {
    api.save(this, {
      content: newContent
    });
  }
});

This seems pretty handy! we don't update our UI until we know the datastore has the up to date state, so are application is now super portable, and multiple people can, in theory, all work on the same data. That's awesome, free collaboration!

The downside is that this is a UI blocking approach, meaning that if for some reason the data store fails, components won't get updated despite there being no technical reason for that to happen, or worse, the data store can be very slow, leading to actions the user took earlier conflicting with their current actions because the updates happen while the user's already trying to make the UI do something else.

Of course, we can reverse the order of commits and UI updates, but that introduces an even harder problem: invalidating the UI if it turns out the changes cannot be committed. While the api approach has neat benefits, they rely on your infrastructure being reliable, and fast. If that cannot be guaranteed, then contacting a data store for committing states manually may be a better solution because it limits the store interactions to bootstrapping (i.e. loading previously modified components) and user-initiated synchronization (save buttons, etc).

Dealing with Component Relations

Parent to Child: construction properties

This is the classic example of using construction properties. Typically the parent should never tell the Child to do things via API calls or the like, but simply set up need property values, so that the Child can do "whatever it needs to do based on those".

Parent to Descendant: a modeling error

In React, this relationship is essentially void. Parents should only be concerned about what their children look like, and nothing else. If there are things hanging under those children, those things should be irrelevant to the Parent. If they're not, this is a sign that the choice of which components map to which abstract concepts was not thought out well enough (yet), and needs redoing.

Child to Parent: this.props

Children can trigger behaviour in their Parents as long as the Parent supplies the API to do so via construction properties. If a Parent has an API for "doing something based on a Child doing something", that API can be passed into along during Child construction in the same way that primitive properties are passed in. React's JSX is just 'JavaScript, with easier to read syntax' so the following:

render: function() {
  return ;
}

is equivalent to:

render: function() {
  return React.createElement("Child", {
    content: this.state.content,
    onUpdate: this.handleChildUpdate
  });
}

And the child can call this.props.onUpdate locally whenever it needs the Parent to "do whatever it needs to do".

Child to Ancestor: a modeling error

Just like how Parents should not rely on descendants, only direct children, Children should never care about their Ancestors, only their Parents. If the Child needs to talk to its ancestor, this is a sign that the choice of which components map to which abstract concepts was, again, not thought out well enough (yet), and needs redoing.

Sibling to Sibling:

As "intuitive" as it might seem for siblings to talk to each other (after all, we do this in traditional HTML setting all the time), in React the notion of "siblings" is irrelevant. If a child relies on a sibling to do its own job, this is yet another sign that the choice of which components map to which abstract concepts was not thought out well enough (yet), and needs redoing.

Deciding on how to propagate data

Chains of this.props.fname() function calls

The most obvious way to effect communication is via construction properties (on the Parent side) and this.props (on the Child side). For simple Parent-Child relationships this is pretty much obvious, after all it's what makes React as a technology, but what if we have several levels of components? Let's look at a page with menu system:

Page -> menu -> submenu -> option

When the user clicks on the option, the page should do something. This feels like the option, or perhaps the submenu, should be able to tell the Page that something happened, but this isn't entirely true: the semantics of the user interaction changes at each level, and having a chain of this.props calls might feel "verbose", but accurately describes what should happen, and follows React methodology. So let's look at those things:

var Options = React.createClass({
  render: function() {
    return 
  • { this.props.label }
  • ; } }); var Submenu = React.createClass({ render: function() { var options = this.props.options.map(function(option) { return }); return
      { options }
    ; }, select: function(label) { this.props. } }); var Menu = React.createClass({ render: function() { var submenus = this.props.menus.map(function(menu) { return }); }, select: function(menu, option) { this.props.onSelect(menu, option); } }); var Page = React.createClass({ render: function() { return (
    { this.formSections() }
    ); }, navigate: function(category, topic) { // load in the appropriate section for the category/topic pair given. } });

    At each stage, the meaning of what started with "a click" changes. Yes, ultimately this leads to some content being swapped in in the Page component, but that behaviour only matters inside the Page component. Inside the menu component, the important part is learning what the user picked as submenu and option, and communicating that up. Similarly, in the Submenu the important part is known which option the user picked. Contrast this to the menu, where it is also important to know which submenu that "pick" happened in. Those are similar, but different, behaviours. Finally in the Option, the only thing we care about is "hey parent: the user clicked us. Do something with that information".

    "But this is arduous, why would I need to have a full chain when I know that Menu and Submenu don't care?" Well, for starters, they probably do care, because they'll probably want to style themselves when the user picks an option, such that it's obvious what they picked. It's pretty unusual to see a straight up, pass-along chain of this.props calls, usually a little more happens at each stage.

    But what if you genuinely need to do something where the "chain" doesn't matter? For instance, you need to have any component be able to throw "an error" at an error log or notifying component that lives "somewhere" in the app and you don't know (nor care) where? Then we need one of the following two solutions.

    Targeted events using the Publish/Subscribe model

    The publish/subscribe model for event handling is the system where you have a mechanism to fire off events "at an event monitor", who will then deliver (copies of) that event to anyone who registered as a listener. In Java, this is the "EventListener" interface, in JavaScript's it's basically the document.addEventListener + document.dispatch(new CustomEvent) approach. Things are pretty straight forward, although we need to make sure to never, ever use plain strings for our event names, because hot damn is that asking for bugs once someone starts to refactor the code:

    var EventNames = require("eventnames");
    
    var SomeThingSomewhere = React.createClass({
      mixins: [
        pubsub: require(...)
      ],
      componentWillMount: function() {
        if (retrieval of something crucial failed) {
          this.pubsub.generate(EventNames.ERROR, {
            msg: "something went terribly wrong",
            code: 13
          }); 
        }
      },
      render: function() {
        ...
      },
      ...
    });
    
    var ErrorNotifier = React.createClass({
      mixins: [
        pubsub: require(...)
      ],
      getInitialState: function() {
        return { errors: [] };
      },
      componentWillMount: function() {
        pubsub.register(EventNames.ERROR, this.showError);
      },
      render() {
        ...
      },
      showError: function(err) {
        this.setState({
          errors: this.state.errors.slice().concat([err])
        });
      }
    });
    

    We can send off error messages into "the void" using the publish/subscribe event manager, and have the ErrorNotifier trigger each time an error event comes flying by. The reason we can do this is crucial: when a component has "data that someone might be able to use, but is meaningless to myself" then sending that data off over an event manager is an excellent plan. If, however, the data does have meaning to the component itself, like in the menu system above, then the pub/sub approach is tempting, but arguably taking shortcuts without good justification.

    Of course we can take the publish/subscribe model one step further, by removing the need to subscribe...

    Events on steroids: the broadcasting approach

    In the walkie-talkie method of event management, events are sent into the manager, but everybody gets a copy, no ifs, no buts, the events are simply thrown at you and if you can't do anything with them, then you ignore them, a bit like a bus or taxi dispatcher, when everyone's listening in on the same radio frequency, which is why in the Flux pattern this kind of event manager is called the Dispatcher.

    A Dispatcher pattern simplifies life by not needing to explicitly subscribe for specific events, you just see all the events fly by and if you know that you need to do something based on one or more of them, you just "do your job". The downside of course is that there will generally be more events that you don't care about than events that you do, so the Dispatcher pattern is great for applications with lots of independent "data generators" and "consumers", but not so great if you have a well modelled application, where you (as designer) can point at various components and say what they should reasonably care about in terms of blind events.

    You promised to circle back, so: what should I go with?

    Perhaps not surprisingly, I can't really tell you, at least not with authority. I have my own preferences, but need trumps preference, so choose wisely.

    If you're working with React, then depending on where you are in your development cycle, as well as learning curve, many of the topics covered are things you're going to run into, and it's going to make life weird, and you'll need to make decisions on how to proceed based on what you need.

    As far as I'm concerned, my preference is to "stick with React" as much as you can: a well modeled centralized component that maintains state, with this.props chaining to propagate and process updates, letting render() take care of keeping the UI in sync with what the user thinks they're doing, dipping sparingly into the publish/subscribe event model when you have to (such as a passive reflector component, like an error notifier that has no "parent" or "child" relationships, it's just a bin to throw data into).

    I also prefer to solve problems before they become problems by modeling things in a way that takes advantage of everything it has to offer, which means I'm not the biggest fan of the Dispatcher model, because it feels like when that becomes necessary, an irreparable breakdown of your model has occurred.

    I also don't think you should be writing your components in a way that blocks them from doing the very thing you use React for: having a lightning fast, easy to maintain user interface. While I do think you should be saving and syncing your state, I have strong opinions on "update first, then sync" because the user should never feel like they're waiting. The challenge then is error handling after the fact, but that's something you generally want to analyse and solve on a case-by-case basis.

    I think you should use state to reflect the component state, no more, no less, and wherever possible, make that overlap with the "full state" that fits your abstract notion of the thing you're modeling; the more you can props-delegate, and the less you need to rely on blind events, the better off your code base is going to be. Not just for you, but also for other developers and, hopefully, contributors.

    And before closing, an example: implementing editable elements.

    Let's look at something that is typical of the "how do we do this right?" problem: editable forms. And I mean generic forms, so in this case, it's a form that lets you control various aspects of an HTML element.

    This sounds simple, and in traditional HTML, sort of is simple: set up a form with fields you can change, tie their events to "your thing"s settings, and then update your thing based on user interaction with the form. In React things have to necessarily happen a little differently, but to the user it should feel the same. Change form -> update element.

    Let's start with something simple: the element. I know, React already has pre-made components for HTML elements, but we want a freely transformable and stylable one. In abstract, we want something like this:

    element:
      attributeset:
      - src
      - alt
      - title  
      transform:
      - translation
      - rotation
      - scale
      - origin
      styling:
      - opacity
      - border
    

    Which, at a first stab, could be the following React component:

    var utils = require("handyHelperUtilities");
    
    var Element = React.createClass({
      getInitialState: function() {
        return utils.getDefaultElementDefinition(this.props);
      }, 
      render: function() {
        var CSS = utils.convertToCSS(this.state);
        return (
    {this.state.alt}
    ); } });

    But, does that make sense? Should this component ever be able to change its internal state? Yes, the abstract model as expressed as, for instance, a database record would certainly treat "changed data" as the same record with new values but the same id, but functionally, the component is just "expressing a bunch of values via the medium of a UI component", so there isn't actually any reason for these values to be "state", as such. Let's try this again, but this time making the Image a "dumb" element, that simply renders what it is given:

    var utils = require("handyHelperUtilities");
    
    var Element = React.createClass({
      getInitialProps: function() {
        return utils.getDefaultElementDefinition(this.props);
      },
      render: function() {
        var CSS = utils.convertToCSS(this.props);
        return (
    {this.props.alt}
    ); } });

    Virtually identical, but this is a drastically different thing: instead of suggesting that the values it expresses is controlled by itself, this is simply a UI component that draws "something" based on the data we pass it when we use it. But we know these values can change, so we need something that does get to manipulate values. We could call that an Editor, but we're also going to use it to show the element without any editorial options, so let's make sure we use a name that describes what we have:

    var EditableElement = React.createClass({
      getInitialState: function() {
        return ...?
      },
      componentWillMount: function() {
        ...?
      },
       render: function() {
        var flatProperties = utils.flatten(this.state);
        return ;
      }
    });
    

    Let's build that out: we want to be able to edit this editable element, so let's also write an editor:

    var utils = require(...) = {
      ...
      generateEditorComponents: function (properties, updateCallback) {
        return properties.map(name => {
          utils.getEditorComponent(name, properties[name], updateCallback);
        });
      },
      getEditorComponent: function(name, value, updateCallback) {
        var Controller = utils.getReactComponent(name);
        return ;
      },
      ...
    };
    
    var Editor = React.createClass({
      render: function() {
        return (
    {utils.generateEditorComponents(this.props, this.onUpdate)}
    ); }, onUpdate: function(propertyLookup, newValue) { ...? } });

    Now: how do we get these components linked up?

    EditableElement -> (Editor, Image)

    The simplest solution is to rely on props to just "do the right thing", with updates triggering state changes, which trigger a render() which will consequently just do the right thing some more:

    var EditableElement = React.createClass({
      ...
      render: function() {
        var flatProperties = utils.flatten(this.state);
        flatProperties.onUpdate = this.onUpdate;
        return (
    { this.state.editing ? : }
    ); }, onUpdate: function(propName, newValue) { var curState = this.state; var updatedState = utils.update(curState, propName, newValue); this.setState(updatedState); } });

    In fact, with this layout, we can even make sure the Editor has a preview of the element we're editing:

    var Editor = React.createClass({
      render: function() {
        return (
    {utils.generateEditorComponents(this.props, this.onUpdate)}
    ); }, onUpdate: function(propertyLookup, newValue) { this.props.onUpdate(propertyLookup, newValue); } });

    Excellent! The thing to notice here is that the EditableElement holds all the strings: it decides whether to show a plain element, or the editor-wrapped version, and it tells the editor that any changes it makes, it should communicate back, directly, via the onUpdate function call. If an update is sent over, the EditableElement updates its state to reflect this change, and the render chain ensures that everything "downstream" updates accordingly.

    Doesn't that mean we're updating too much?

    Let's say the Editor has a slider for controlling opacity, and we drag it from 1.0 to 0.5. The Editor calls this.props.onUpdate("opacity", 0.5), which makes the EditableElement call setState({opacity: 0.5}), which calls render(), which sees an update in state, which means React propages the new values to the Editor, which sees an update in its properties and so calls its own render(), which then redraws the UI to match the exact same thing as what we just turned it into. Aren't we wasting time and processing on this? We're just getting the Editor's slider value up into the Element, we don't need a full redraw, do we?

    Time to repeat that sentence one more time:

    If used correctly, your users will think they are manipulating a UI, when in fact they are manipulating React, which may then update the UI

    In redux, this means we did not first change that slider to 0.5, and so we definitely need that redraw, because nothing has changed yet! You're initiating a change-event that React gets, after which updates may happen, but the slider hasn't updated yet. React takes your requested change, kills it off as far as the browser is concerned, and then forwards the "suggestion" in your event to whatever handles value changes. If those changes get rejected, nothing happens. For example, if our element is set to ignore opacity changes, then despite us trying to drag the opacity slider, that slider will not budge, no matter how much we tug on it.

    Extended editorial control

    We can extend the editor so that it becomes more and more detailed, while sticking with this pattern. For instance, say that in addition to the simple editing, we also want some expert editing: there's some "basic" controls with sliders, and some "expert" controls with input fields:

    var SimpleControls = React.createClass({
      render: function() {
        return utils.generateSimpleEditorComponents(this.props, this.onUpdate);
      }
    });
    
    var ExpertControls = React.createClass({
      render: function() {
        return utils.generateExpertEditorComponents(this.props, this.onUpdate);
      }
    });
    
    var Editor = React.createClass({
      render: function() {
        return (
    ); }, onUpdate: function(propertyLookup, newValue) { this.props.onUpdate(propertyLookup, newValue); } });

    Done. The Editor is still responsible for moving data up to the EditableElement, and the simple vs. expert controls simply tap into the exact same properties. If the parent is rendered with updates, they will "instantly" propagate down.

    And that's it...

    If you made it all the way to the bottom, I've taken up a lot of your time, so first off: thanks for reading! But more importantly, I hope there was some new information in this post that helps you understand React a little better. And if there's anything in this post that you disagree with, or feel is weirdly explained, or know is outright wrong: let me know! I'm not done learning either!

    http://pomax.github.io/#gh-weblog-1433888478288


     

    Добавить комментарий:
    Текст комментария: смайлики

    Проверка орфографии: (найти ошибки)

    Прикрепить картинку:

     Переводить URL в ссылку
     Подписаться на комментарии
     Подписать картинку