In the Mouth of the Beast
I have worked with React.js for a while now. This library has gotten so much adoption, and it’s no longer in its infancy. Major companies have adopted React as a core piece of their application beyond Facebook: Twitter, Airbnb, Dropbox, Walmart. It’s here and I have the distinct impression it has more staying power than the adoption of almost any other framework or library.
But why do teams adopt React? I think it’s a combination of working incrementally on UI and state management is weird on the client side. Since React has a very small surface area where its API is concerned.
The massive adoption led an increased assortments of different types of applications and vastly different needs centered around the simple API.
Managing state can be complicated but talking about it might be more complicated if we’re not clear on what state is or represents in an application. What is state? State can be anything. It’s a description of what’s going on in your application. We can capture anything we want in state: is this modal open? did an error occur? what posts did our API respond with? is this setting on or off? what text is in this input field? We could be here awhile, so I trust you’re starting to get the idea.
In React, Components compose other Components. and those components have descendants. Every component can manage its own state or be handed that state via a prop
. You can lift up a descendant components’s state to an ancestor.
In this example we’re passing the state of submitting
down the descendant and elevating the state of the field
to the the Ancestor component.
class Ancestor extends React.Component {
state = {
submitting: false,
error: null,
};
onSubmit = (payload) => {
this.setState({ submitting: true });
fetch("/endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
.then((res) => res.json())
.then((response) =>
this.setState((state) => {
let submitting = response.status === 200;
return {
submitting,
error: !response.status === 200,
};
})
);
};
render() {
return (
<Descendant submitting={this.state.submitting} onSubmit={this.onSubmit} />
);
}
}
In this component, we disable the input if the form is submitting so that input value doesn’t change and on submission we grab the state of field
for the fetch request.
class Descendant extends React.Component {
state = {
field: "",
};
onSubmit = (e) => {
e.preventDefault();
this.props.onSumbit({ field: this.state.field });
};
render() {
const { field } = this.state;
return (
<form onSubmit={this.onSubmit}>
<input
value={field}
disabled={this.props.submitting}
onChange={(e) => this.setState({ field: e.target.value })}
/>
</form>
);
}
}
These examples are simplistic and obviously a little contrived. But what if we had to make this state known the whole of our application (we’ll refer to this as application level or global state)? We’d have to continually pass state down from Ancestors many levels up, which can get nasty and hard to track and force us to use shouldComponentUpdate()
to keep this in check so our more nested components aren’t re-rendering unnecessarily.
As more applications grow and depend more on React the more global state we might need.
Raising the Beast
One of the reasons I resisted learning React while I was working on an Ember.js application, was that at the time (mid-2015), a lot of promotion around React by other developers was always React + another library; React + Flux, React + Alt, and finally React + Redux. React was getting billed as something to complement other pieces of an application architecture versus something that could stand on its own. The original React announcement was about using Backbone.Router
and React at Instagram, whereas at the time Ember 2 and Ember Data was a complete solution for the application I was building, the subliminal message about React was it wasn’t a solution outside of something.
So as someone not using React consistently and skeptical about changing my workflow or tools while I was already semi-productive, the message was to use React; you need a lot of other moving pieces to build an application.
Turns out that’s not true. When building an application with React and using a polyfill’d fetch (which is available in most modern browsers), React Router (which is just some components) can get you pretty far with very little overhead.
I don’t want to be dismissive of using Flux or Redux. A lot of teams have been very productive with these tools and built very cool applications with these tools.
But I think there’s maybe a fundamental flaw in beginning with a state management solution versus growing into that need. One thing that I found true of my experiences working in an application using a global state management solution, is there’s a sunk cost fallacy associated with making these choices.
While I was working on a team using Flux and React, we had the constant conversation about trade-offs we made in previous sprints committing to building Flux stores and the boilerplate code we had to write to setup that pattern. The cost to eject was too high to consider refactoring when we ran into issues. Once we had this pattern setup, the temptation to continually add to the global stores was way too within reach and too hard to resist. This made our team mistrustful of using a Component’s localized state to handle basic UI-level state (like toggling checkboxes and inputting text in input fields).
The other issues I kept finding to be true is that having props
passed through the connect()
method from Redux or the Store.getState()
from Flux makes it harder to manage where the state we’re passing down or handing to lower level components is coming from, and prevents us from naming things very clearly. Being verbose shouldn’t be a crime.
In the 3 bigger applications I’ve worked on, complexity becomes a beast that’s hard to tame. Then when it comes to adding features or refactoring components / reducers the understanding of which components get touched by your changes is harder to get a handle on and raises the cost and time of working an application over time.
Getting Swallowed Whole
For most of 2017, I was working on an application that used Flux and React that we inherited from an offshore team. It was engulfed in Flux stores and getting global variables rendered from a .NET app sprinkled in the codebase. Each feature took whole sprints, technical debt was a little hard to calculate and unit testing was a little bit out of the question because the components weren’t well defined.
React wasn’t necessarily a defined point in our front-end stack application was swallowed by no real authentication flow, tying UI state (like open modals and input fields) to a Flux store and server rendered global variables.
While I was at React Rally, Michael Chan gave a talk called “Back to React: The Story of Two Apps”. He talked about his experience working on applications at Planning Center and kept talking about React not being a missing piece to fill the UI layer of an application but the totality of your application.
He outlined 5 liberating constraints to approach your application:
- Know whose mouth you’re in / who’s driving — tries to answer the question of what role does React play in your application. Is it controlled by a server side framework passing down props or a state management solution or a client side router? Understanding who owns the pieces of that application and where they come from is a critical piece of productivity.
- Optimize for change — defining the criticality of a component and noting the pre-optimization of abstraction. Not easily or eagerly abstracting your components. The cult of re-usability is real folks.
- Know your component shapes and master the patterns — There a few patterns React that are really popular presentational and container components or knowing and unknowing components, Higher order components, Function as Child and Render Props (which is more or less the same as FaC). You should know them all and understand which is more needed than the other in your use case.
- Avoid the edges — knowing your edges. Your ability to be productive with tools with libraries that you can master. Adding newer libraries will often let you feel all pain of them as they battle test their concept. Choose tools that enable productivity and lower overhead.
- Partner don’t depend — the idea that you should stop delegating to OSS and accept that when you
yarn install
a dependency you’re adding another piece to take ownership over. Likewise, to guard your stack from unnecessary dependencies which goes back to avoiding the edges.
He brought up this quote:
Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better. — Edsger W. Dijkstra
Simplicity isn’t easy. It’s a struggle to maintain and curate. This helped me understand the beast our team raised and failed to tame. Complexity and short paths swallowed the needs of our application and we lost the thread of our own productivity.
Climbing Out of the Mouth of the Beast
If our best answer to not creating a beast out of our front-end application is abstinence then it’s a non-answer; ie. just don’t use Flux or Redux. Human nature favors the discipline of abstinence like meteoroids favors resisting gravitational pulls — we all end up crashing and giving in at some point.
But understanding how to limit the needs of your application is the first step. We can start tame the beast by limiting the influence of a global state library. Writing a list of all the things that need to be global in your application; usually that’s like a JWT, the user’s timezone or some user preference like a current theme. Likewise understanding what good use cases of a state management system are are really important, there’s a great blog post called You Might Not Need Redux that is definitely worth reading.
Most applications don’t have much global state, but a lot of applications have state related to current view they’re in (via a client side router and React Router is great for this).
The other upcoming thing we could employ is the new Context API being proposed and there’s a polyfill you can experiment with today. Context has been a part of React since it’s inception, it’s not really widely used because it’s documentation comes with the following warning:
If you want your application to be stable, don’t use context. It is an experimental API and it is likely to break in future releases of React.
Context lets us pass down state on as-needed basis by calling into the context
of an Ancestor component as high up in the hierarchy that we like (even the top level component). In theory, we could have a top level presentational component that has state passed into context and anywhere we’d like in our Route components (via React Router) call into that context. Which I honestly believe that this is the future of working with global state.
Our applications can get out of hand quickly. The cost of adding features needs to remain low for ongoing productivity. Mastering your application and taming it is an important tenant for keeping that cost as low as possible.
I personally believe that state management solutions are overused and they need to have as light of an influence as possible on your application. And the more I keep to that central tenant, the more productive I’ve found myself being in an application’s lifecycle.