These days we have dynamic and modern UIs in our Web Apps. We learned that the best way to build reactive and reliable UIs is probably going with a Component-based strategy. Now each component has a simple responsibility and behaviour that renders a piece of the interface, like Lego pieces we break them and merge again later to build a feature.
With that in mind we are able to build large-scale Web Applications. We build a Style-guide to have a maintainable, reusable and consistent front-end. We use (or not) a library/framework of our choice to help us in that journey, we learn the best practices. But we still need to make a decision…
How will components interact with each other?
Hmm… You, probably, already caught yourself thinking about this. What decision did you make? Is there a best technique to follow?
Scenario
Imagine you have something like this: each component is independent and may interact with each other, not just a parent-child communication. What are the options to we communicate between these components?
Circles are components. Green components should update its behaviour after a change on Orange component.
Let’s take a look on how some popular libraries encourage it:
- Angular 1 & 2: Callbacks and Event Emission;
Vue.js: Callbacks and Event Emission or a Flux pattern;
Ember: Callbacks through Actions;
Backbone: Event Emission.
After a look, it seems that our options are:
- Callbacks/Props
Event Emission/PubSub
Flux Based patterns
I’ll use some examples to play with these techniques and it can be applied to any component-based system. The text below is totally opinionated based on previous experiences.
Callbacks
Parent components can pass attributes in to a Child component.
Using callbacks/props it’s very simple to communicate between Parent-Child components, like the example below, you can just pass a function as a property to the Child component.
Lets take a look in this example using React.
class Parent extends Component {
doSomething() {
console.log('It works!');
}
render() {
return (
<Child handler={this.doSomething.bind(this)} />
);
}
}
class Child extends Component {
render() {
return (
<div onClick={this.props.handler}>Child</div>
);
}
}
You are communicating from Child to Parent, but what about the opposite direction? It can be done with references:
class Child extends Component {
doSomething() {
console.log('It works!');
}
}
class Parent extends Component {
render() {
return (
<div onClick={this.refs.child.doSomething}>
<Child ref='child' />
</div>
);
}
}
The problem is that it makes the parent and child tightly coupled. It won’t scale well, because, like in our scenario, it’s common to have complex component trees. We don’t want to pass it down manually, from the Grandparent to Grandchild component. And the scenario hasn’t only a parent-child relationship.
React Context
React provides a workaround to solve this problem, It’s called Context. It can solve the deep nested communication problem, but will probably cause other issues. Like React’s docs says, it should be avoided. It’s *in some way *similar to having a global variable. It isn’t safe.
Angular Services
In Angular’s world it’s common to extract logic into services/factories. Angular has a dependency injection system, you could use it to inject some logic only into the component which you want. Components using the same service can share data and behaviour. No component relation needed.
However it’ll share it through Angular’s $scope, and it isn’t callbacks, so we’ll talk more about it on Event Emission section.
Too Much Responsibility
Another limitation of using callbacks is that when our scenario is complex we can have too many things being done in a single callback. Imagine that the Child component should do one thing for its Parent and another for Grandparent. It’ll lead us to have a huge callback.
function doSomething() {
console.log('Do something for your Grandparent');
console.log('Do something for your Parent');
console.log('Do something for your Siblings');
console.log('It works!');
}
We can’t easily split these behaviours since we have a callback function. We could put the logic in different functions, however we would still have to call all of them in the callback.
Based on what we saw until now, we can summarize it:
Callbacks/Props
- Pros: Very simple, easy to understand/debug.
Cons: Limitations on non parent-child communication; To much responsibility in a single callback; Nested callbacks/props is definitely a bad thing.
Event Emission
When we think about components communicating independently, event emissions probably comes in mind.
Its theory is simple: components will have access to some sort of event bus where they can listen to and emit events. When emitting, it passes a label (an identifier) and optionally some data.
var bus = new EventBus()
// Product component when clicked emits
bus.$emit('add-to-cart', product)
// Cart component listens
bus.$on('add-to-cart', doSomething)
function doSomething(product) {
// ...
}
The difference here compared to Props is that the components don’t need to be related. They only need to handle same event label, emitting and listening to outside, as a protocol of the communication.
The good win is if 4 other components must be updated after a change they can just listen to the event and execute their logic independently, each one taking care of its own single responsibility.
Too Many Events
While Callbacks are simple, events will bring us new things to prevent. Every event listener will need to identify which component is triggering it, and if it wants do something with this data.
Because sometimes, when an event is emitted, you may or may not want to handle an update, however the event will be triggered either way.
We should be carefully when listening to generic events. I remember working on an Analytics module where we wanted to push to our system information about all tooltips being opened. This module was listening to a “tooltip_opened” event, but there were some specific tooltips that we don’t want to push, although, as they were tooltip components too, they were triggering the tooltip_opened event too.
I can naturally think in two possible solutions for this. 1. Identify the kind of tooltip in the Analytics module listener, or 2. Create a variation of tooltips that emits both “tooltip_opened” and “analytics:tooltip_opened” events, using this variation only for tooltips that we want to push information.
We can’t just emit the “analytics:tooltip_opened” event in the variation, because some other components may be listening to the “tooltip_opened” too, and we want to trigger them no matter what kind of tooltip is being opened.
var bus = new EventBus()
// Tooltip component
bus.$emit('tooltip_opened', this)
// ... other events
// Tooltip-variation component
bus.$emit('tooltip_opened', this)
bus.$emit('analytics:tooltip_opened', this)
// ... other events
// Analytics module
bus.$on('analytics:tooltip_opened', pushInfoToServer)
Other common thing that happens is when you ran into an infinite loop of events, then components start to update itself more than needed, sometimes infinitely. It’s hard to debug.
Event Emission
- Pros: Components totally independent; Easy way to pass arbitrary data to other components.
Cons: Too many events are hard to debug; More boilerplate code.
Flux Based Patterns
Flux is an architecture popularized by Facebook. It uses an unidirectional data flow, and the way that components will communicate or handle changes is defined by it.
Besides it’s being planned to work with React, this concept works for any Component-based system. Because, like I said, it’s a concept, being able to be adapted for other libraries.
- You can learn more about in the Flux website.
Now this subject area has a huge ecosystem. After Facebook made it public, a lot of Flux implementations emerged, like Alt, Reflux, Fluxxor and of course the Facebook implementation. It isn’t just that, Flux influenced other concepts too, there are adaptations of the Flux core, like Redux and MobX.
Well, generalizing, the way they work in the high level is with the Actions, Store, and Views concepts.
Actions are “Events” being dispatched/fired when something occurs. Unlike the Event Emission architecture, components don’t listen to events, they all are dispatched to the Store (the “State Management System”). The View (components) will get the state from the Store as properties and render it, when the state changes, it happens again. All of this working in an unidirectional flow.
// Application code
const store = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
const render = () => {
<Counter
count={store.getState()}
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>
}
store.subscribe(render)
// Counter Component
const { count, onIncrement, onDecrement } = this.props
<span>Clicked: {count} times</span>
<button onClick={onIncrement}> + </button>
<button onClick={onDecrement}> - </button>
As we can see it adds an extra level of complexity, because it ins’t only managing communication, it manages all the application flow, dealing with the state updates too. It means more code, meaning more tests and more time to maintain it. Could it be overengineering in some cases?
You will know when you need Flux. If you are not sure if you need it, you don’t need it.
— React-howto
I don’t know if it can be applied to any kind of applications or how healthy it is in the long way. I’ve made some experiments with React and Redux/MobX, then moved to Vue.js and Vuex and had no problems with both until now, in fact, I’m really really enjoying it. But they were just experiments.
Here is an example of a “shopping cart” using Vue and Vuex that I played with, if you’re interested on how it works with more logic.
Flux Based
- Pros: Components are totally independent; Easy to debug (great tools); No event mess.
Cons: Way more complex (the learning curve); More code; It’s a whole architecture (and it isn’t mature yet).
Conclusion
After all, in my opinion there’s no best answer (I know, that is a cliché), it’s hard to make this choice, it depends on which kind of application you’re building and the libraries you’re using.
Callbacks seems to be a good fit for small applications, however, I think can be hard to maintain -because of some limitations- as the project grows.
Events provide us what we need, scales well, but if we not take care it can be a mess after some time. Imagine a lot of events popping up and you having to deal with this flow, then some components trigger duplicated updates and you don’t know why.
I’m still not sure if it’s better to use one or another, or a mix o them. What I can say is that by now I’m stick with the flux alternatives. It provides me what I need when creating UIs, and the idea behind it seems more solid than Event Emissions.
Another thing that I’m paying attention at the moment is the Elm Architecture, impressive! It can definitely be a subject for a next post.
Do you know any other alternatives? I’m curious about how are you doing it today. Leave a comment 🙂
We want to work with you. Check out our "What We Do" section!