The introduction of React Hooks has sought a more declarative style of programming and promoted the use of functional components.
However, as our applications scale, our code becomes harder to understand and maintain. Issues of duplicate code, out of sync data and incomprehensible tree structures quickly plague our nicely designed components.
By introducing programming patterns we can improve the architecture of our application and ensure that our components do not get bloated with irrelevant logic.
- What is a programming pattern?
- Why use programming patterns?
- Observer pattern with React Hooks.
- Provider pattern with React Hooks.
What is a programming pattern?
“A software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design“ — Wikipedia
A programming pattern is simply a well-defined solution that you can apply to your code for a range of situations.
Programming patterns are agnostic of any language — they do not include the code you need to write, but rather an overview of what classes/objects you can use and how they can influence each other. In front-end development there are 3 main types of patterns.
Creational — Patterns that give you flexibility over creating objects
Structural — Patterns that help us with structuring classes and objects — What belongs to what? How can an object access another object?
Behavioural — Patterns specifically concerned with communication between objects
Why use programming patterns?
Design patterns are tried and tested methods that have been used for decades. Their use cases are clearly highlighted and they can help you solve existing problems very quickly.
Patterns themselves also provide a strong basis for developers to communicate in. Code becomes self-documenting when you use familiar pattern names in your code. Reading third party packages and your colleagues code becomes easier to understand once you recognise relevant programming pattern terms and interfaces.
For instance, the following terms are spread throughout the React code base.
A developer familiar with the above paradigms could quickly identify the design and use of a function without having to dive into the fine grain details of the code.
“The Observer Pattern is useful in cases where there are multiple objects (or modules, or components) that are dependent on a single piece of state”
The Observer Pattern — Answering Three Whys
This is useful in React when you have 2 disconnected components that you want to keep in sync with each other.
The Observer pattern relies on a source of truth. We call this the Subject.
An Observer will attach itself to the Subject and wait patiently.
When the Subject changes, it will go notify all the observers with the latest data.
For this example we will create a website which tracks soccer scores.
We want our app to display the live goals in one section of the app and also display the history of goals in another section. Whenever a goal is scored, we want both sections of the app to update at the same time.
For demonstration purposes we can imagine that these components lived very far apart from each other.
This is how we will structure our code using the Observer pattern.
In our example above we are attaching a list of callback functions to the Game Subject. Whenever we
score a goal, these callback functions will be fired one at a time with the latest goal data.
If you are unfamiliar with React hooks, head over to Introducing Hooks
When our ScoreBoard Consumer is initially rendered, we attach the
onGoalScored callback to the Game Subject. When a goal is scored, the callback function is triggered by the Subject, which runs the
onGoalScored on the Consumer and updates the score.
Similarly, in our GoalHistory Consumer we attach ourselves to the Game Subject and wait patiently for any goals to be scored. When a goal is scored we update the list of
goals with the
Team that scored and the
time it happened.
By using observers we have ensure that both of these components remain in sync with each other
Low coupling between components — Components do not need to be in the same tree to remain in sync with each other.
Promotes one way data flow — changes are triggered in one place and are easy to track.
Memory leaks — when a component is no longer in use we need to make sure that we
detach from the
Subject to avoid unused observers.
The Provider pattern was initially used within .NET Microsoft applications to provide a class with a range of ‘condiments’ (props) to be used on initialisation. Since then it has been reframed in the context of React as a useful pattern that enables components to stay in sync with some ‘global’ state.
It is useful when you want to make a common object available to multiple components in your app and force these components to update when the object changes — React Provider
The main benefit of this pattern is avoiding having to pass a value through the props of every component in the React tree, otherwise known as “Prop Drilling”.
The way it works is that a Provider sets some values on a Context object that is set on a parent level of the component tree.
Any of the child components , called Consumers, can then grab those values from the context directly, instead of having to pass the values through the props of each of the children.
For this example we will provide support for small screen devices in our soccer website from the previous example. When the screen becomes a certain size, we will restrict the amount of content that can be seen.
We will create a
DeviceContext object which contains the state we want to share to all the Consumers. Next we set up the
DeviceProvider which allows us to inject the state into a given area of the application. Whenever we use the
DeviceProvider , all of our children (Consumers) will gain access to the
In practice you would separate the Context object and the Provider into their respective files.
We use the React
createContext hook to set up our object we want to share throughout our website.
DeviceContext is an object which has a screen size, either
large . On a screen resize event, we change the screen size if the width drops below a certain number of pixels.
Next we create the
Provider which shares the context to all of the children rendered inside of it. Using React’s built in
Provider API we create a simple Higher Order Component that sets the
value property of our
DeviceContext. This value is available to all children rendered inside the
In order to consume the
DeviceContext we must wrap our application in the
Provider component. Now both ScoreBoard and GoalHistory have access the the provided device object.
Inside of our GoalHistory component we can now use React’s
useContext hook to gain access to the Device Context object imported from our Provider Class.
useContext API ensures that the component will stay in sync with the object being provided and the value of
We do not need to worry about manually updating these components as “all consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.” -React Docs
Prevents prop drilling — less coupling between parent and child components
Promotes one way data flow
Invisible complexity — it is hard to tell which component is the parent and where the data is coming from
Promotes use of globals — lowers cohesion throughout your app, React docs recommends using
Whilst patterns are great for certain scenarios, they are also often abused by developers. Patterns do not scale well, they solve small design problems quickly and elegantly but become hard to maintain when your application grows.
Don’t let patterns guide you, only use the pattern if it fits the problem at hand - used unwisely, they are often overkill.
- Break your code into smaller components
- Keep data flowing one way
- Focus on the ‘Single Responsibility’ rule
- Keep it simple
Used correctly, patterns provide an intuitive solution that is quickly understood by a larger community of developers. Usage of patterns, especially in React, can enhance the communication between your components, ensure a single state is propagated throughout the entire app and help you avoid any disorganised tree structures.
Go forth and learn the diverse patterns that are available. By understanding a variety of patterns, you will increase the amount of solutions ready at your disposal and improve your architectural capabilities beyond component design.