Lessons Learned Working With React

I’ve recently done a lot of work with some fairly large-scale React projects. This is a collection of miscellaneous lessons I’ve learned working with React and React Native that I can’t find a great spot to put as part of a larger work. Instead, I’ll compile these ideas here and flesh them out further to hopefully reinforce some good practices in my colleagues and other React developers in the wild. There are some excellent good-advice React articles out there including the React documentation itself, and a lot of these ideas have also been acquired working with Angular and similar technologies where the concepts can be carried over. There are too many to remember and list, but a couple of recent ones I’ve appreciated are:

These ideas primarily apply to working with a standard React setup using TypeScript and Redux with RxJS. Anyway, here is my own list of ideas in no particular order.

I will include code examples from GitHub gists inline. These will either be a self-contained example or two blocks of code — a bad example followed by a good example. In some cases the first example will not necessarily be bad or cause errors, it’s just that I have justification for preferring the second good example.

Perform Logic in Particular Spots

Logic is a very broad term, and there are many different examples and ways to define it .When I talk about logic in this case, I mean what others may refer to as business logic (a term I viscerally dislike), or the logic that drives the decisions made in your application with respect to its data. This can include things like calculating the total amount of an order, adding an item to a list without duplicates, and anything else you can think of that essentially takes some starting data and transforms it into something else. Logic should be focused into the following spots essentially in-order, although I’ll go into more details about what kind of logic should go where.

  • Reducers — application logic for manipulating data
  • Connect functions, mapState/DispatchToProps — transform data for use by components. If you find yourself having to transform data a lot or across all components that use a reducer slice, you may want to update the object shape of the slice in the reducer instead.
  • Epics/Sagas — Logic unique to handling asynchronicity, e.g. debouncing and canceling
  • Components — Minimal logic required for conditional display and iteration.
  • Services — Any logic consistently required for interacting with data not used directly by your application (that can be isolated to the services layer). This could include retrying requests or reauthenticating.

I’ll expound on each of these in a bit. I just want to point out that figuring out where something should be done in your code-base is always a challenge. The lines between these different types of logic and how they can be handled are always blurry. Sometimes it comes down to a guess as to whether something should be done in a particular spot and in a particular way that is going to have the most benefit later. A lot of times, you’ll guess wrong. Other times you will be lazy or simply make a mistake. That’s okay, and the lesson you may learn could be more valuable than anything you’ll read here.

Maximize the work done by Reducer Functions (Redux)

You want to do as much as you can in your reducer functions. There are definitely limits to what you can do, but a primary question you can ask is whether what you need to do involves taking data from one state and transforming it into a different state. If you can answer yes, you probably want to do that work in your reducer functions. There are tangible benefits to maximizing work done by reducers:

  • Reducers are composable. Don’t forget about your access to and the power of reducer composition much like you shouldn’t forget component composition (also discussed later).
  • Reducers are reusable. Redux also has a push-based philosophy that forces data to travel through reducers for transformation first. This allows it to push required data onto various components rather than various components pulling data and updating it in their own way. This means that the data that flows through your application will be consistent across all reducers, and updates done by reducers will impact all subscribed components.
  • Reducers should be stateless. As long as they are, they are very easy to test. If they’re doing the most important / logically complex work of your application, you get the most value out of testing them from known inputs and outputs.

You should almost always be asking yourself “can this be done in a reducer?” It’s also important to keep in mind that you can break the logic of your reducers out into multiple functions. You don’t need one giant function that does everything for a reducer.

In the above first example, the Profile component is doing more work than it needs to by doing an existential check on the name, upper-casing the name for display, and lower-casing the name for storage. Instead, this work can all be done by the reducer. We have the advantage of transforming the application state in various ways that we see fit and exposing those to any components that may need them rather than making them the responsibility of the component. If we’re careful in our reducer, we can guarantee a sane default for all state properties which allows us to skip existential checks in components.

Finally, don’t forget that more than one reducer can respond to a single action. It makes sense to keep actions logically grouped and specific, but you should disabuse yourself of the notion that an action should only be consumed by a single reducer (or Epic while we’re at it).

Transform data for Components in your connect functions

The react-redux library’s connect function takes two arguments: mapStateToProps and mapDispatchToProps. These two functions take application state from your reducer and transform it into an object for use by a component, and take a dispatch function and transform it into an object with methods for dispatching actions, respectively. These are propagated to the connected component’s props. There’s a necessary transformation that goes on here — specifically, you’re getting properties you need for a component from a larger set of properties. This may involve renaming.

It can be helpful to export these functions for testing, although you may also choose to simply have code coverage ignore them since their work should be largely trivial.

This data transformation will involve renaming or flattening as minor transformations, but it should not do anything else. Beware if you find yourself performing more complex transformations on the data you receive. That should be done in the reducer instead.

This first example is similar to the previous first example except that the work / logic is being done in the connect functions. In the second example here, we move all of that functionality to the reducer instead and simplify the connect functions and the data transformations as much as possible. This will allow us to easily reuse the transformations across components if they’re needed.

Handle asynchronous data and errors in your Epics

I have only worked with redux-observable (Epic is the term for the asynchronous-handling function), but I think this could probably apply to redux-sagas and other similar side-effects handling libraries. There is some logic that needs to be done / some decisions that need to be made with respect to the asynchronous flow of data and error handling for something async.

Keep in mind that all of the data that moves through epics will ultimately pass through reducers as specified by actions emitted from your epics. This means that application logic done in your epics can most likely be done in your reducers.

We move the data transformation logic to the reducer mostly because it’s easier to test than the epic. It also ensures that the same transformations are done even if the data comes from a different place. Data for a particular application state slice must flow through your reducers, but it doesn’t necessarily reach your epics depending upon how you’ve set them up.

Components and their own brand of logic

It’s inevitable that you will have some form of logic in your components. Eventually you’re going to need to display something conditionally. There are different ways to write this, such as using the branch HOC from the recompose library, but when you get down to it there’s going to be some form of logic required for displaying your application properly based on the data.

That said, there is one key type of work to try to avoid in your components: data transformation. Most likely you will want to do this work in your reducer instead and have the data ready when the component actually needs it. We’ve done a couple of examples of this with the profile information reducer and component display above.

I think that this is the most common mistake, if I can call it that, that people make when working with React and Redux. Redux can certainly keep the application data in more than one state if it needs to be used differently among different components. This kind of work done in components is more difficult to reuse and test than if it were done by reducers. If you do some transformation in the reducer and some in the component, you’re also spreading your logic into different layers.

Services do their own logic isolated to the services layer

Redux has not supplanted services, it’s just that while I used to store and transform application data in services, I now keep it in Redux’s application store and transform it via reducers. However this only applies to application state. We still need services to drive our application.

I come from an Angular world, and people at least used to tend to think of services as being modules that stored and manipulated data acquired from an API. While this was true, and I still use services for at least the API interaction piece, services have always been about the encapsulation of functionality. These don’t have to include working with any persistent data at all. There are some obvious ones such as Angular’s Http service, but there are others people don’t really think about such as Renderer and ErrorHandler. There are also suites of third party services including Ionic Native that handle interactions with device APIs such as geolocation and camera. Even moment/date-fns and geolib can be considered services in this context.

We still need services, and they will probably still have to do quite a bit of logic in some cases. Just ask yourself a question before you do work in a service: is the result of this work isolated to the service? As long as you keep manipulation of data that comes in and that you send out consistent and it’s only changing in the service / services layer itself, you’re probably good to go.

This goes in line with the question about whether work can be done in a reducer. As long as it relates to application data, being able to do it in the reducer supersedes being able to do it in a service. This is mainly because reducers are easier to test. You also get more consistency since all data must flow through your reducers.

Avoid Logic In…

Now that you have a list of where you should be doing what kinds of logic, there are a couple of particular spots to avoid doing logic at all:

  • Action creators. If you find yourself needing to do logic in an action creator, it can be done in a reducer. You may have to rethink the action and how it’s dispatched. The point is, if additional logic is required to properly dispatch an action, you’re destroying reusability by isolating the logic to a specific place where that action is used once. This is similar to doing too much logic in connect functions.
  • Components … sometimes. As stated above, you will inevitably have to do some sort of logic in components. There is a concept of smart/container components that work with application state. You should isolate these from the dumb/presentational components that handle displaying, formatting, and styling. I don’t think that this is trivial to do, and I would say that I’m honestly pretty bad at it. It’s just nice to have components that do nothing but style some props that you can plop around pretty much anywhere in your app that you can trust to work consistently in spite of your current application state.
  • Pretty much anywhere else I didn’t name. I can’t think of much else except for maybe application bootstrapping, configuration, application store creation, etc. You should strip down any work done in these areas and anywhere else you can name to the bare minimum. These are not only difficult to test, they also have the largest surface area since your entire app necessarily depends on them. Minimize your chances for mistakes here.

Avoid Reference Literals in Render Functions

By “reference literal” I mean any literal declaration that creates a reference such as an object literal, array literal, inline function, object created by new, and essentially anything that isn’t a string or a number. The reason for this is that it can trigger additional render cycles by comparing two different objects even if they have the same value.

If you need to create a handler function that depends on a prop, you can do it by calling an instance-bound function that returns a function like so:

However, this does actually create a new function when compared to the old. This is of course necessary if the prop value changes — the function will be different after all. If you need to be strict about it, however, you can memoize the function created from a specific set of parameters so it doesn’t get recreated with each render using the same parameters. There are libraries that can help with this too such as https://github.com/alexreardon/memoize-one

To be honest, strictly adhering to this philosophy can be a bit annoying at times. React purists will think it’s sacrilege, but if you’re using a literal in a render function every now and again and it has no noticeable performance impact, what’s the harm? Just keep in mind that in addition to being potentially more performant, your code may be easier to reason about if you move some literals to properties, stylesheets, et. al.

Use mocks

This applies to jest specifically, but it could probably apply to any testing framework more generally. I was so used to dependency injection in Angular and how it eased testing, that I totally ignored the philosophy behind working with React and jest. Dependency injection is essentially not needed because of jest's mocking capabilities.

Rather than create an injectable class, mock your service, and inject the mock, you can simply mock the service at the module level to have the same effect. Since React doesn’t have a built-in dependency injection mechanism and there isn’t one widely supported or agreed upon by the community, using jest mocks is a better way to handle this.

I have learned this lesson, but I want to explore it more before providing examples. You can provide simple, flat mocks via jest.mock in a setup file. Doing this in test files doesn’t seem to work as I expected initially.

If you have a renderX method it can be a component

Having renderX methods inside components alongside their own render method always seemed a bit untoward to me, but I couldn’t quite put my finger on it.

If you see yourself creating a renderX method for a component, it could be a separate component instead. You don’t even have to put this component in a separate file or anything, but you might find that it makes sense to do so.

Having separate components gives you more opportunities for reuse and it’s easier to test as well. It also makes it easier to extract where and how props are used for both the parent component doing the rendering and your rendered child component. Moreover it looks better, at least in my opinion.

If the child component needs properties from the parent, you can pass them as props rather than accessing them on this in a renderX method directly. Any time you find yourself writing a renderX method in a component that already has a render method, ask yourself if you can break it out into its own component. Most likely it will be trivial to do so.

I have seen examples (including with React Native’s own FlatList) that have renderX methods that essentially just render another component. Continuing the example above, this might look like:

export class RenderComponentsComponent {
...
  renderList = () => <ListItem items={this.props.items} />
  render() {
return (
<View>
{this.props.items.length && this.renderList()}
...

You don’t always need an additional indirection method to render something. When passing a function to a render prop, you may want to use an instance bound method. See the Avoid Reference Literal section above for an explanation.

Share styles through components

While this applies to React Native specifically, it can also be applied to React more broadly. It is even the basis for a lot of CSS-in-JS frameworks where styled components can be created from bare components.

On the web you still have the option of using CSS in any context, but this is not the case with React Native which handles styling with a different paradigm. It precompiles stylesheets. This means that stylesheet objects cannot be trivially passed around and reused across files where they are created — they are specific to components. Since React Native stylesheets are created from object literals, those can be passed around, but the general recommendation is to reuse styled components rather than the styles themselves. This requires an adjustment in thinking particularly if coming from CSS-land.

At a minimum you get a bonus from reusing the component in that you don’t have to create a StyleSheet from the style object in every component where those styles are needed. The responsibility for styling is isolated to a component rather than having multiple components extract and manipulate style objects for potentially varying results.

You need an escape hatch from componentDidUpdate

Using componentDidUpdate with Redux is very common. This is a replacement for componentWillReceiveProps where you update how your component is rendered based on updated props. Since Redux maps application states to props, triggering of componentDidUpdate would indicate a potential update of application state since you’ve received new props.

Calling setState will also trigger componentDidUpdate. This requires the need for a condition where setState is not called. In the title I call it an “escape hatch,” but this is just a reminder not to call setState if it isn’t needed since that will cause an endless loop. This is also mentioned in componentDidUpdate's own documentation.

Remember to review your componentDidUpdate code to ensure that there is a path where setState is not called, and it doesn’t continuously update. This might happen even if setState is not called in the componentDidUpdate method itself! componentDidUpdate might call a method that ends up calling setState.

You probably don’t need a constructor in TypeScript

This is a minor semantic thing specific to TypeScript, but you probably don’t need to use constructor for components and many classes in TypeScript. You can do property initialization including using this inside the property definition parts of the class. This includes setting initial state and default props.

I have a personal preference for avoiding constructor, although there are a lot of examples of components, particularly written in JavaScript, that do use it. The property initialization in TypeScript essentially runs the constructor, so the difference is purely structural. You just don’t have to remember to call super() if you’re not using constructor, and you avoid extra indentation.

This mostly applies to React components (and Angular components for that matter). However, you don’t need a constructor for initialization as long as you don’t depend on any constructor arguments.

Setup / tear down in lifecycle methods

Typically you will set things up in componentDidMount. This will include creating Observable subscriptions to respond to changes. This doesn’t need to be done for application state since it will be updated via props (componentDidUpdate, see above).

When you set up a subscription or handler in componentDidMount, remember to remove the corresponding subscription/handler in componentWillUnmount. Otherwise, you will have a memory leak and the handler may continue to fire even if the component is not rendered anymore. Rendering the component again may cause duplicate responses via subscriptions/handlers.

Not every statement in componentDidMount creates a subscription or listener, but when writing and reviewing code, make sure that these are appropriately set up in componentDidMount and removed in componentWillUnmount. In cases where they are set up outside of componentDidMount, they still need to be torn down in componentWillUnmount. This may justify the creation of a component to handle the subscription too.

The event loop is your friend … but it might betray you

Understanding the event loop and code synchronization in JavaScript is important for any application — particularly as it gets larger. I have written an old but still applicable summary of synchronization here:

It’s important to be able to mentally walk through the asynchronous flow of the code you are authoring and reviewing so you can see possible issues.

Can you spot the issue in the code above?

The problem is that stop() is called immediately when the epic is created. Instead, we only want to call stop() in response to the corresponding stop action. We can solve this pretty easily by changing switchMapTo to switchMap(() => which will call stop in response to the action instead of as soon as the epic is created. Try to be aware of the context in which function calls are made.

Embrace the power of component composition, but beware of deep nesting

This is very high-level advice that’s more of a reminder to think about how you are composing components. Try to keep the following principles in mind:

  1. Only expose props to components that use them. Avoid prop drilling by using context (Redux uses context).
  2. Props can be exposed to descendants through composition.
  3. Minimize the responsibility of components. Keep logical/container/smart components separate from presentational/dumb components. Generally speaking your “dumb” components are your reusable ones. Your smart components are used in specific spots that drive particular application functionality. Tying a component to particular application state intrinsically diminishes its reusability.

You also want to keep a sane level of abstraction by avoiding too much nesting / too much composition. It’s impossible to say what too much is without knowing details… it’s just good to think about your code and consider whether you or others can reason about it. Minimizing the responsibility of components can help, but you don’t want to do this by sacrificing clarity.

From the examples above, my least favorite is the first example since this uses so-called prop drilling. Components have to pass props to components they render only so those subsequent components can use the props. The components themselves don’t need the props. In the second example, we solve this via context (a Redux solution would be similar). In the third example, we don’t break out into separate components. This limits reusability. The fourth example solves this problem.

My preference is for something like the final example — it’s a compromise between breaking out parts of components that can be reused and composing them together for specific application components that may rely on application state such as a user’s name. We also aren’t creating too many additional components that we won’t end up reusing anyway.

Remember that nothing is perfect

I hope that the advice in this article is useful to some people, especially the people that I work with and have trained in these technologies. You should also remember that no matter what you do, the code you’re going to write is not going to be perfect for a variety of reasons. You’re constantly learning. In order to learn how to write better code, you have to write worse code first. You’re also always under some sort of time constraint… at the very least you’re not going to be developing an app forever since you won’t live forever!

In this vein, it’s okay to accept some level of imperfection and compromise. Don’t beat yourself up if you see something that you know you could do better, even if you don’t know how to do it better yet. Refactoring should be an ongoing process, and if you’re not doing it as well as you’d like, don’t beat yourself up about that either.

While you accept that nothing can ever be perfect, I think you should also continuously strive to get as close as possible. Don’t accept present failure as a justification for avoiding future problems. Try to be objectively critical about your work, and criticize your work, not your identity.