React & React Native: Mastering Design Patterns & SOLID Principles for Effective Software Development
Want to master creating web and mobile applications in React and React Native? You’re in the right place. Although one of the advantages of React and React Native lies in them being declarative, that doesn’t necessarily mean that you’re going to write the most efficient and maintainable code. In this article, I’m going to take you through how to up your code with design patterns and SOLID principles using an easy-to-follow example and some free code.
The SOLID Design Principles
Useful Design Patterns
Project Tooling for React and React Native
In the Beginning… there was MVC
Originally, React was advertised as the V in MVC (Model View Controller) but the React team has since discontinued using the phrase. The phrase implies you should use some other technology for the “M” and “C”. Today however you can use the Container/Component pattern. Container in this context acts as the Controller and as the Page/Screen itself (I’m going to refer to Pages and Screens as just Screens from this point.) Containers compose UI and handle the business logic. The pattern itself is reasonable, sound, and necessary. After all, you have to group components up somewhere.
Our early forays into the Container/Component pattern yielded mixed results. It did the job, but there were times when we ran into complexity problems at the Container level.
We’d hit problems where features would compete with react state and also mental real-estate. The Components themselves were mostly fine as they were “dumb” enough but on occasion, this wasn’t always the case, overly competitive state management caused unnecessary renders and tracing state changes throughout an app was a real chore. In some situations, they had an unhealthy dependency on their parent Containers/Components. Redux helped, but also brought another layer to traverse with actions triggering effects that triggered more actions.
The result is something that works, but the code sprawls across business logic and the jarring paradigm shift from React to (old style) Redux meant it wasn’t a pleasant experience. Onboarding staff onto such a project can be time-consuming and difficult, relying largely on prior knowledge to make sense. Unit Testing was painful at the Container level and even more so at the Redux & Saga/Thunk level. Obviously, we had to improve the process to reduce cognitive load, the conflicting patterns, and improve the testing experience.
At this point, we set about on a period of experimentation and learning to overcome these challenges. To summarise, the key problems we wanted to solve were:
Disparate and sprawling Business Logic layer. Business Logic existing in Containers, Components, and Redux (Sagas/Thunks).
High Cognitive Load coming into and out of Containers. Complex action flow and state are set in multiple functions, interwoven with Redux.
Lacklustre Testing. Testing methodologies differ between Components, Containers, Sagas, and high-level coupling making tests arduous, error-prone, and long-winded.
Back to basics with SOLID Principles
SOLID principles are a set of concepts that form key guidelines for the effective development of software in Object-Oriented Programming (OOP). At KOMODO, we use these principles to great effect in non-React-based projects. So, we know there is a lot to gain from following some of these principles for React projects too. The SOLID principles are:
Single Responsibility Principle (SRP): An entity should do only one thing, changes should not impact the overall system.
Open-Closed Principle (OCP): An entity should be open to extension, closed to modification.
Liskov Substitution Principle (LSP): Objects should be replaceable with objects of their subtype, without changing the correctness of usage.
Interface Segregation Principle (ISP): Many specific interfaces are better than one large interface.
Dependency Inversion Principle (DIP): Depend on abstractions, not concretions.
So, not all of these principles are applicable or necessary, nonetheless, we can still gain improvement from them to ease our woes. For example:
SRP applies to our entire codebase. Components can (and arguably should) do only one thing.
OCP applies to all Components and Containers directly as data transfer is unidirectional (props are open, state is closed).
LSP could work well as we are using TypeScript (JS would let us do anything we want without any type checks which makes it riskier.)
ISP again won't exactly be necessary for Components/Containers except deriving our props.
React prefers composability over dependency injection so DIP isn’t applicable, although we can use it if it becomes beneficial. Existing means of dependency injection rely on injecting through a Component's props like MobX inject or through Redux connect which both inject a store as a prop.
SRP and Containers
At this point I want to focus on the application of SRP to our Container problem, after all, we’ve already identified that it was doing too much, so SRP can help in this circumstance.
It's important to note that this is specifically referring to classes and our container is a component, not a class. However Components are similar to classes, as a class is just a template that creates an object, which is exactly what Components do, so I’d argue that this principle does apply.
So, with SRP in mind, what does a Container do?
As I’ve mentioned previously, the Container acts as the Controller and as the Screen itself - composing UI and handling the business logic. This is a lot, but it all breaks down. We don't have to perform the business logic in this Container, it just needs to handle it. So we can instead think “Containers compose UI and business logic” and its goal is still being met. Therefore a Container’s single responsibility is one of composition only.
So we need to compose UI and functionality. Components are easy, they do this already. Composing functionality however is another matter.
Enter Design Patterns
A design pattern is a solution, but before I start talking about the pattern of choice, I just want to issue some words of caution. Design patterns are like herbs and spices, using a select one or two at the right points will make your food taste much better. Using all the herbs and spices will make your food taste confusing and wrong, and this is the same with design patterns.
Attempting to use them all will make your software confusing and incredibly brittle. However, using the right one at the right point can do a world of good. A pattern implemented correctly will feel natural and will just get out of your way.
I highly recommend learning about them if you haven’t already(we’re going to a bit of that here), so you can recognise them when you see them, identify when to use them, and when not to.
SOLID principles in practice
We’re going to take the ever-present LoginScreen as our example journey throughout this article. It can demonstrate our problem without being overly complex from an understanding perspective.
Let’s take a look:
I know, I know. I can already hear the baying calls for using Redux (Thunk/Saga) / MobX through the internet. I'm getting to that, stay tuned. This example shows what a login screen does and I want to discuss the application of SOLID principles to break it down.
In our example the screen is doing a lot of stuff:
Displaying the Screen UI to the user.
Creating an on mount effect.
Creating two callbacks for press events.
Reading the last used email to prepopulate the email address when the screen is mounted.
Reading the last used remember me setting and setting the rememberMe state when the screen is mounted.
Setting the last used remember me value when the switch is toggled.
Setting the email and password for the form.
Submitting the form login details when submit button is pressed.
Showing an error if the login fails.
Navigating to the next screen if the login succeeds.
Navigating to the forgot password screen if the forgot password button is pressed.
Just to reiterate, the example above is pretty simple but it's not difficult to see how other screens could grow incomprehensible when complexity increases. The example ignores navigation as it's not important for what I want to highlight. It could be react-navigation or react-native-navigation or whatever, as long as it can be used as a consumable service.
From a composability standpoint, only a few of these need to exist within the container. The container must compose the UI, that's a given. In addition, because the form is built up by the container, the necessary state changes to fields are required too, we have to collect the information after all.
We also need to show an error if there's a problem. Now, this is where it gets interesting. The container requires nothing else. It's the container's job to show a form, collect information and show an error if there's a problem. The container needs to present this screen to the user.
It is NOT the container's job to submit this information to the API, or to handle the response to determine what the error is, if there is one; why should the screen care where it sends its API call? It just needs to know the request is being made, and the relevant data from the response. It is NOT the container's job to determine what the last used email was. The container just needs to know if there was one, and what it is; how it gets it or where that data comes from doesn’t matter. So this functionality can be pulled out.
We can use many tools to do this: Redux (with Thunk or Saga), MobX, plain hooks, also Recoil, a state management library by Facebook looks interesting and could do the job.
For the behaviour we’re trying to achieve, the Command Pattern is best suited. The Command Pattern is used to encapsulate an action or request for execution at a later stage. Redux uses something roughly similar; it uses actions to trigger code that is decoupled elsewhere, which could be reducers or effects via thunks or sagas.
MobX supports transient stores which can observe and provide observable states and execute actions. A regular hooks method would rely on wiring up useEffect, useState and useCallback (et al) hooks and provides the easiest path to refactor our code, we’d just call this custom hook instead.
Why use the Command Pattern?
Commands are their own entity, they do their own thing.
They’re reusable and indifferent to what the caller is. Most of all, you don't need to go far to see what it does. It also fits with our composability goal without having to worry about messy shifts in code structure. It provides lightweight guide rails to not be overbearing and restrictive but provides enough familiarity to be recognisable when we see them.
There are pros and cons to all tools - and some grey areas. Let’s go through some of them here and discuss:
Hooks based Command:
Simple refactoring path.
No additional libraries are required.
Dependent upon the caller being a functional component and is tied to the consumer’s render cycle.
Hooks based commands cannot be swapped out based on state changes and attempting to do so will lead to additional complexity.
Supports time travelling and immutability.
Can be combined with hooks style Command composition.
Not dependent upon React component type.
Well established with good debugger support.
Dependent upon 3rd party libraries (Thunks/Saga/Persist) for effects and storage which can be difficult to test and have their own pros/cons.
Many Commands can pollute the single global store, Commands will not be self-contained.
Requires sending additional actions to switch state coming out of effects to preserve the purity of data. This means, on the one hand, your data is kept pure, on the other, it means that your action flows are more complicated; especially in complex side effects.
Can compose a Command in a single object
Not dependent upon React component type.
Not dependent on global stores.
Dependent upon 3rd party libraries (Persist)
Limited debugger support, less established ecosystem.
Data is not naturally pure, you could potentially run into race conditions if you’re not careful with state management. Including an additional library, MobX-state-tree can help here and allows flexibility to do both.
The pattern we’re implementing has roots in OOP, so it would stand to reason that MobX would be the better fit here where each store is its own object. It will allow us to compose Commands that are not tied to a global state and allow the creation of transient stores which are ideal for the Command Pattern.
Redux has many advantages but it is not a good fit for our purposes, it is more suited to functional programming, where the additional code flows are a more natural fit. Going further, if you’re tied to Redux, I would actively avoid using it for a Command Pattern implementation and use plain react hooks, and consume global state from there, which is its intended use case. Using plain old hooks for Commands is also not a natural fit, because it is dependent upon the consumer being a functional component, it introduces consumption problems and testing difficulties with it being tied to render cycles.
I think the higher-order function pattern would be a better fit for toolings like Redux and regular hooks so behaviour can be mocked effectively in testing and is a more natural fit for functional programming. Given MobX’s flexibility, we’re going with this for our implementation.
Applying the Command Pattern
So taking the login process in the above example, let’s make our LoginCommand. Before I do, I’ll just want to create our Command shape which will be used for all Commands.
So, we can see our Command exposes two methods, execute and canExecute which accept parameters. The method canExecute here prevents unnecessary execution of the Command. It checks all kinds of things like its own internal state, currently executing requests, multiple executions etc. The execute method is our main body of work.
People migrating from Xamarin, WPF, and UWP backgrounds will recognise this, Microsoft uses this pattern heavily in their MVVM-based UI frameworks for the reasons I’ve already mentioned. We’re not going down the full MVVM route though, because React is its own thing and I think MVVM will be too overbearing and contrary to React application architecture, but nothing is stopping you from using it if that's your preference.
In addition, I've added a small utility hook, which uses MobX-react under the hood to return our observable store. This will allow our container to easily consume the Command.
There is also a CommandInvoker which is a higher-order function just used as a helper for calling a Commands to execute method to stop writing the same repetitive code.
The LoginCommand itself looks like this:
This looks similar to our first example, with some minor changes. Firstly we’ve introduced two properties: pending and error. The error property replaces the useState call. The canExecute method performs any validation on our params and checks to make sure the call isn’t already in progress. The execute method performs the fetch, updates our last used email, sets our pending observable while executing, and finally sets the error if the request is unsuccessful.
Our ForgotPaswordCommand now looks like this:
After composing these Commands into our container, our LoginScreen now looks like this:
Great, now our container doesn’t care about what happens to our credentials after we’ve entered them, the Container just cares that they pass to the right Command. This is an example of High Cohesion.
It also doesn’t care how the credentials are being validated either. If our business rules change and we have to check for minimum length, for example, the Container doesn’t care why they’re invalid or not, only that they are. Our Commands are also consumable from other Containers/Components too. This is a good example of Low Coupling.
Now I need to point out, that it's not entirely decoupled, we’re still dependent upon the Command having error and pending properties. You could abstract these out to expose an AuthenticableCommand for an agnostic implementation. If your app exposes multiple ways of authenticating (Sign in with Apple, Google, Twitter or Facebook login for example) you could then implement these separately as different AuthenticableCommands and swap to suit implementation whilst keeping our Container focused on composition and UI.
Likewise, you can implement DIP here to inject our commands if required. We’re not going to do that though, preferring composability, but the option is open.
N.B. Our Container is also now an observer, so it can watch our observables in the Commands we’re using.
We’re not quite done on this, the screen is thinking about how we’re pulling back the remember me functionality. This functionality stores data in AsyncStorage and reads the value and prepopulates the email field. The LoginCommand is also concerned about the need for storing this information. Before we go into details about pulling that out, I want to take a moment to talk about Layered Architecture and MVC patterns.
Common Layered Architecture describes how a system breaks down into higher archetypal roles as layers such as:
Data Source / Domain
These roles can differ slightly on a system by system basis. Layered architecture often has its own proponents and detractors, the arguments are typically about web application infrastructure regarding scalability affecting and changes affect all tiers, growth and evolution, and deployment.
MVC is an application design pattern where Views interact with Models and Controllers and Controllers interact with Views and Models. Models are manipulated by Controllers.
Layered Architecture and MVC have similar goals but some would argue that MVC occupies only the Presentation layer while others argue that MVC is a type of Layered Architecture.
The arguments for not using Layered Architecture are less pronounced or moot altogether when dealing with just the React frontend. For example, application evolution will occur regardless of the pattern used, and the frontend will need to change to compensate regardless to accommodate new features.
Layered Architecture will help manage this evolution in the app in a controlled manner, where changes are trickled down the stack. As we’re also only dealing with the app it will all have to be deployed in one go anyway.
Finally, React uses a unidirectional data flow, so a layered architecture is a more natural fit and helps achieve one of our goals of lowering cognitive complexity. This is why Redux is a popular choice, actions are unidirectional and state changes are propagated back on each change. Technically we are still using MVC as our architecture, but with some additional layers on top of the Controller to manage the application better.
Adding layers to the application
Going back to our example. Based on the arguments above, this is what we have at the moment:
Now we need to add a layer for our Data Access and Data Source layers. The Data Access layer will hold our Models and store any data from responses to requests coming from the Data Source layer, such as authentication tokens, in our example. I'm not going to talk about the security implications of where you store that data, that's not the point of this article, so I'm just going to store it directly in the Model using MobX-persist (Local Storage) in these examples but you should definitely exercise due diligence and store sensitive stuff securely.
This is what we should end up with:
Under this, our LoginCommand is doing too much again. It shouldn’t care about where it gets the data or how the data is stored. It should only care whether or not the request is successful so it can perform the correct action.
So let’s extract a Model for our User to store our authentication data and set up our data source. We’ll start with our data source first. We’re going to create a general response object so we can create an abstraction with fetch API:
And with this, the login request looks like this:
Our Data Source is easy to implement. All we’re doing is extracting the fetch call to a new function, nothing special. However, now the consumer doesn’t care about its internal configuration. When an error is thrown it's because of some network related problem, not because of an error status. I’ve left off handling network errors because it's not important to what I want to convey, which is that the domain call’s only job, is to pull back API data in a format which can be safely consumable for the rest of the app.
In turn, our User Model looks like this:
It's just another MobX store however we’re exporting as a singleton. This Model assumes hydration will be handled elsewhere, it's not necessary to show this for the topic though so I'm ignoring it.
So now our Model can process the data and persist the necessary info. In terms of SRP, the Model's job is to only manage data for the User which includes getting the data, storing data, and notifying its dependents when data changes. Using this User model, our LoginCommand now looks like this:
Now our Command only cares about if the action was successful or not, and what the error is. Here you can add your own error for consumption by the Screen. Much is the same as it was but now we have narrowed its concerns markedly. We can add a new Command for our remember me functionality too:
This is a simple Command that just sets the rememberMe flag on the Model. Nothing more, simple. Finally, our LoginScreen becomes this:
In this, you can see we’re composing logic at the top through Commands and executing them with the necessary params at points of interaction. Furthermore, each interaction is consistent and predictable. We could easily refactor this to execute Commands the same way for each handler. We’re also now pulling persisted information from the Model. The screen no longer performs its own functions, it is complexity free.
So, what have we achieved?
You might be thinking that all we’ve done is shift code around and created multiple new steps. And you’d be correct. That's exactly what we’ve done, but we’ve done it on key input/output points on each layer. It’s trivially easy to discern what something is doing and what its job is, as everything has only one singular job. When developers only have to reason about a singular entity during development, the risk is reduced, and they’re more likely to catch things that would’ve gotten lost in the noise. Things like, where is this data being sent?
We’ve also reduced the cognitive load on developers working with the system. Interactions go up the layers through Commands and data comes right back down the layers, it's that simple. We’ve implemented a standardised, flexible and repeatable means of writing business logic, via Commands and Models. This means a developer will know what to expect when viewing a container and can be reasoned with in logical stages.
In a complex screen (unlike our example), it allows for easier reasoning of what's coming from where and what is displayed. It also allows for the easier realisation of different interaction models. Pull to refresh, swipe to delete and more can all be modelled and wired up without tripping over each other’s state.
Furthermore, we can create templates for different files in our layers to reduce the burden of facilitating such a system, scaffolding tests, Commands, Models, Screens and API calls, each following the same familiar patterns. Teams can write consistent code that everyone understands. This reduces management risk and your team won't be dependent on key players in the team. Likewise, team members won't be railroaded onto projects because they “know the system”.
We’ve promoted reusability across the board. Commands can build on top of our Models. More screens can build on top of those Commands. Moreover, because each Container, Command, Model and API call has only one job, testing and mocking become exceptionally easy to scope. Developers are less likely to miss complex edge cases because of this, further reducing development risks.
You can mock each Command's access to the Model for example, and verify its result to check multiple code flows. You can mock the Models call to the API trivially to test multiple responses. If you want to know what happens on a 401, you can go right ahead and do that without having to check everything. With this increased testing capability, we can aim for 100% test coverage without having to create monolithic test suites that try to see what happens across the board. For example what happens to a screen when receiving a 401?
The answer: the screen shouldn’t care about that, only that there was a failure.
If you’re ever in a situation where you can’t make heads or tails of what something is supposed to be doing, it might be time to step back, break it down, and think:
If this only has one job, what would it be?
Sign up to our newsletter
Be the first to hear about our events, industry insights and what’s going on at Komodo. We promise we’ll respect your inbox and only send you stuff we’d actually read ourselves.