Architecture
This follow-up on the comparison between component patterns in React and Ember.js discusses the concept of React Hooks and if the same concept can be applied to Ember.js when creating clean component abstractions.
As a follow-up on my post on React Component patterns in Ember.js , I want to take a look at React Hooks and compare them to component abstractions that the Ember.js framework provides. Usually when React Hooks are discussed one gets the notion that they are a very powerful concept that enables developers to create elegant abstractions and solve “hard” problems. Mostly people talk fondly about the concept of Hooks so I wanted to find out what they are about.
This is not a post that is meant to offend anybody that is working in the React ecosystem nor is this post meant as a hype post for the Ember.js framework. As discussed in a recent blog post of mine I think it is interesting how different client-side framework communities solve common problems when implementing single-page applications. I looked into the topic of React Hooks from the perspective of a long-year Ember.js developer and treated my research as a learning opportunity about a framework that is foreign to me.
When implementing complex client-side applications you want to build up your User-Interface in a systematic way by reusing the same abstractions all over your application.
Usually people only think about Design Systems when it comes to reusability in client-side applications but presentational UI-components aren’t the only things that you want to make reusable. All across your application, there are common behaviors that you want to use over and over again - like triggering an async interaction or showing a notification based on a particular user-interaction - and you want to make these patterns reusable as well.
So coming up with good abstractions in single-page applications doesn’t just mean creating reusable presentational components like buttons but also to make it easy to reuse certain stateful logic across your application.
Unfortunately finding good abstractions and composing them together is challenging and sometimes the framework you are using makes it even harder:
Before Hooks for example, according to the React core team , React didn’t provide good abstractions to share stateful logic:
React doesn’t offer a way to “attach” reusable behavior to a component (for example, connecting it to a store). If you’ve worked with React for a while, you may be familiar with patterns like render props and higher-order components that try to solve this. But these patterns require you to restructure your components when you use them, which can be cumbersome and make code harder to follow.
So, are Hooks the only good way to share abstractions across your React application? And more importantly, how does Ember.js compare?
One of the things that nearly all client-side applications have to deal with is asynchronicity. To spare our users a long wait-time when booting up the application, we often times defer to loading data only when the users actually need them or we send async requests to our backend-API when we want to persist data that our users put in via a form.
We want to make it easier to deal with asynchronicity in our application, so lets imagine we got the idea to create a reusable abstraction that “solves” this issue for us. To do this we want to create an abstraction that holds the state of an async interaction and lets us trigger an arbitrary async behavior somehow.
To make the following abstractions with React and Ember.js most comparable I decided to model the asynchronous behavior of our application with a statechart .
This way we can be sure the components behave the same way and compare the actual implementations better. We will be using the popular XState library to model the statechart. This is the statechart that we will be using across this post:
The statechart isn’t doing too many interesting things. We are dealing with
four different states in our implementation of our Async
-component - idle
,
busy
, success
and error
. Whenever we are in idle
we can trigger an
async
-trigger function that we need to pass into the XState Machine
via
the machine’s context
. This request can either resolve
or reject
which takes
us into the success
- or error
-state. When the request resolves we will
trigger an onSuccess
-handler, when the request rejects we will trigger the
onError
-handler.
First, we will take a look at how we can implement a reusable component that
models async behavior with React. We will compare two versions - one that is
using React.Component
and one that is using React Hooks
. We do this because
the official documentation of React introduces
Hooks as a superior way to make stateful logic reusable and we want to test this
hypothesis.
Both versions display the state that the Async interaction finds itself in and a button that can be
used to trigger an async interaction. When clicking the button the interaction
will randomly resolve
or reject
. You can try out both implementations in
the following CodeSandBox:
As you can see both versions of the Async
-component behave the same way. This
leads us to the first important point in this blog post:
Sharing reusable logic via component abstractions in a clean way is possible when
relying on Hooks
as well as it is when using React.Component
.
Let’s dive into the implementations:
To make the state of the async interaction available to the template layer we will make use of the Provider Pattern .
First we need to create an Async
-component that starts an XState-interpreter
when it gets rendered to the page. Via React’s Context-API
we create a Provider/Consumer
-pair that provides the state
-property of the component to other components that want to use the Async
-behavior.
Overall this adds quite a bit of boilerplate because of the way the
Context
-API works but to me doesn’t read too bad, isn’t hard to reuse or hard to follow.
Let’s see how React Hooks compare:
To create a custom useAsync
-hook we create our own abstraction on top the
useMachine
-hook that @xstate/react
provides. Creating your own custom Hooks by composing exisiting ones is encouraged
and one of the selling points of React’s Hooks abstraction.
As you can see creating a custom Hook is pretty simple. We use the
useMachine
-hook, setup our XState-interpreter and then return an object that
we can then use in our application code to handle async
-behavior.
To do this we have to create a component that uses the use-async
-hook though.
At some point, something has to call our hook and it doesn’t seem to be possible
to do this from the “template”-layer directly like it was possible with the
Async
-provider component.
To me, the implementation with React Hooks conceptionally doesn’t look much
cleaner than the React.Component
one. Both versions abstract
the stateful logic of an asynchronous interaction away but I don’t see why doing this cleanly
wouldn’t have been possible already with a dedicated stateful
object when using React.Component
without the introduction of the
Hooks concept. Although often introduced that way, Hooks don’t seem like
a concept much easier to understand than native javascript classes and
some
people in the React ecosystem seem to agree
.
Ember.js doesn’t provide a hook abstraction so to model async
-behavior we
will use a similar approach as we did with React.Component
and create
an Async
-component that will hold the state of our async-interaction.
When looking closely, the implementation in Ember.js is a mix of the two
different implementations in React land. We use a hook-like abstraction
useMachine
that the ember-statecharts
-addon provides but then profit of the fact that we are dealing with a stateful
component instance that can hold properties and functions of its own.
useMachine
in this example doesn’t create anything special but an object that
holds the XState
interpreter state
. We assign this to a property of the component
instance and can then make use of Ember’s autotracking feature
and add getters
like isIdle
that will automatically update when the state of the statechart changes.
To make the state of the Async
-interaction available to consumers we use
Ember’s yield
-functionality to provide
block parameters
to the component block. The caller of the component can then decide which
properties that the Async
-component provides they are interested in and use
them accordingly.
At the time of writing this post - there’s an
open RFC
for Ember.js that suggests to
add functionality to Ember.js that might look similar to React Hooks. With the
@use
-decorator and the Resource
base-class you will be able to implement
stateful abstractions that can be @use
d by other objects in your application.
The important part about @use
in the context of an Ember.js application is that
a used Resource
will be tied to the lifecycle of the object that is calling
@use
. This is nice to have when writing complex Ember.js applications but not
a change necessary to work with stateful objects in your Ember.js codebase that
make it easier to compose behavior.
So in short - @use
and Resource
are not Ember.js’ answer to React Hooks and
quite frankly I personally don’t feel like React Hooks are such a revolutionary
programming paradigm in the first place. To me, Hooks seem more like a band-aid
over shortcomings that React has conceptionally. In React it is pretty verbose
to share state and stateful logic between different parts of your application
when using React.Component
because you have to rely on React’s
Context
-API to do so. Because sharing state via a Provider/Consumer
pair is
so verbose it is understandable that Hooks are a welcome concept to a lot of
React developers but conceptionally they don’t seem to me like a paradigm that is
generally superior to what other frameworks like Ember.js provide.
Again, as in my React Component Patterns -post, I am happy to report that it is possible to create as clean component abstractions with Ember.js as it is with React. The suggested Ember.js implementation even has advantages over both React implementations.
First, it is much less boilerplate to write than with the React.Component
implementation. There’s no need
to create a Context
because Ember.js’ yield
-feature allows you to share
component properties and functions with the template-block in a simple way.
Secondly in contrast to React Hooks, Ember.js makes it easy to build up reactive component abstractions
with its auto-tracking of getters whereas with React you have to update the state yourself whether you were using React.Component
or React Hooks
.
I feel fellow javascript developers, because of React’s prominence, often default to the idea that React leads the way in innovation when it comes to clean abstractions in single-page application development. When investigating a bit more thoroughly I don’t feel this to be a fair assessment. At least not when comparing with Ember.js.
React Hooks, while kind of an interesting concept, simply don’t feel as exciting to me as they seem to feel to React developers because they solve problems that don’t exist when working with Ember.js. React Hooks are certainly a breath of fresh air when you are used to the need of creating a lot of boilerplate to do bread and butter stuff like sharing state in your component tree but that has never been an issue with Ember.js in the first place:
Ember’s yield
-functionality allows you to pass arbitrary data around that callers then can use as they
please. With that, it is easy to create provider components that implement
stateful behavior and make it available to other parts of the application.
When the need to access global application state arises, Ember.js provides services that you can inject into other parts of your application via dependency injection.
Creating abstractions that you can compose and use from other abstractions also doesn’t seem revolutionary to me. It has always been possible to create stateful objects in javascript that follow the single responsibility principle and to compose them. You don’t even have to know about the Rules of Hooks when doing so.
Overall I’m very happy about how well Ember.js Octane compares to React when working on the same client-side application challenge. The framework provides developers with the necessary tools to create concise component abstractions without the need to write a lot of boilerplate code and makes it easy to share stateful logic across different parts of your application. The interesting thing about this post’s exploration to me has been that the abstractions used for the Ember.js component have been around in the Ember.js framework for a long time. There’s no new exciting feature that was added in the recent past that finally made it possible to achieve with Ember.js what React promotes with the Hooks pattern. This might be of interest to people that never would have considered Ember.js for their next upcoming projects. I guess sometimes it is a good idea to compare frameworks on real-world scenarios and don’t decide on your technology stack based on what’s the latest trending opinion on HackerNews.
Please share your thoughts about this post with me on Twitter - I’m always interested in hearing about your experiences when working with React in comparison to Ember.js and your opinion on which one of the two you feel is easier to use.
If this post has sparked your curiosity and you consider Ember.js a possible fit for your next project don’t hesitate to . We are here to help teams build ambitious applications and can help you prototype solutions so you get an idea about which library or framework fits your problem domain best.
We are here to enable your team to deliver ambitious applications. Let's discuss how we can help.