When I first learned about React, I thought it was such an amazing tool. It made everything so much easier. I still think it's pretty great, but when I went back to the first React app I wrote for work, I realized just how little I understood about using it.
Components stay mounted
One of the most important things I didn't understand was that each DOM node represented by a component has a 1:1 relationship with that component. This means that, as long as that DOM node sticks around (there isn't a node of a different type rendered in its place and it isn't removed entirely), React will use the same component instance for it forever.
This particular app was for ops managers in each market to coordinate delivery schedules with drivers. That's all it did, so the content never changed structure much. This means that the components we used, for the most part, stuck around for the life of the app.
We decided to keep a lot of the data in component state because the app itself was simple. Each block of time on the schedule had some metadata on it, like the number of drivers we needed to be available and the minimum amount the driver would make for being available for deliveries (in case their commissions + tips don't reach that value) for that time segment. Since
props are meant to be immutable and we needed to be able to modify some of this data, we simply moved the
The problem is that, when adding a new feature, after days of screwing around trying to figure out why something continued to render stale state, I realized that
getInitialState wasn't being called. Turns out, it is called when the component is mounted and then it will never be called again on that component. This makes perfect sense, but is confusing if the lifecycle of a component doesn't match what you think it is. At the time, we thought we'd be getting new components on each render. Somehow, this didn't cause any problems until we began adding new features to it last week.
Once I realized what the problem was, I began iterating on it to make it easier to work with, but I just succeeded in making more of a mess. First, I tried using
componentWillReceiveProps to take the new props and update the state — something like this:
ScheduleHeader = React.createClass
getInitialState: -> @props
componentWilReceiveProps: (nextProps) -> @setState nextProps
But this resulted in consecutive renders (
componentWillReceiveProps is caused by a render, then we call
setState which starts another one) and it didn't work as well as I thought it would. Then I tried bypassing
setState and just using
this.state = nextProps inside
componentWillReceiveProps. This wasn't any better.
I tried several other equally shortsighted approaches, everything I could think of to make it possible to work with the component in the way it was currently implemented. But it just ended up fixing one bug and causing another. This entire schedule header needed to be gutted. And then, because of the way the header worked with the body of the schedule, it also meant that we needed to do the same there. I was so discouraged I had to go ask Kyle, the one who originally paired with me to write it (we were both learning React together), to help me out.
The good news is that Kyle is one of those developers who never seems to get discouraged by things like this. We ended up rewriting most of the header components (and changing the structure of ones we didn't rewrite) to use state stored in a Redux store, but we got that nearly done in just a few hours.
The lesson here is that, when people recite the React mantra "prefer props over state", this is one of the reasons why. Your component will receive new props on every render, but it might not receive new state because
getInitialState will only be called once since it gets reused on the next render.
Component structure can be deceiving
One of the ways we organized our components originally was that each header field determined on its own whether it would render text or a form input to modify its value. I don't have the code in front of me, but it was something like this:
NeededDriverCount = React.createClass
# The EditField component takes all the data needed to perform an AJAX request.
else if @state.saving
And then I did the same for the other attributes. Each attribute had its own component that did almost the same goddamn thing. So. Much. Duplication.
We ended up refactoring that into a single component that did the same thing for each field, where we could just pass a value and a callback:
ScheduleHeaderHour = React.createClass
EditableField only needs to know the value it's displaying or editing (which one is rendered is based on its own internal state) and a function call when the user presses Enter while editing. That's it! I had originally tried to make the
EditField too smart in some ways and not smart enough in others; it constructed its own AJAX request but didn't determine whether it was displaying text or an
EditableField component does the opposite: it determines what to display but lets an
api object actually update the model on the server.
This refactoring was simple mechanically, but it put everything in the right place conceptually. Sometimes, the way you name things has a lot of influence on how you work with them.