Programming Patterns with React Hooks

Adam Hannigan

Adam Hannigan

Engineering Team Lead

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.

Outline

  1. What is a programming pattern?
  2. Why use programming patterns?
  3. Observer pattern with React Hooks.
  4. Provider pattern with React Hooks.
  5. Disclaimer!

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

Design Patterns: Elements of Reusable Object-Oriented Software

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.

  • ProvideContext
  • EventEmitter
  • Listener
  • Subscription
  • Dispose

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.

Observer Pattern

“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](http://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.

Observer patter diagram

Example

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.

Fifa score

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.

game diagram

The code below uses Typescript. However, the same concepts can easily be applied to plain javascript.

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

Pros

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.

Cons

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.

Provider Pattern

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.

Provider diagram

Example

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.

Provider pattern example

We will create a DeviceContext object which contains the state we want to share to all the Consumers. Next we set up theDeviceProvider 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 DeviceContext

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 small or 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 DeviceProvider.

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.

The useContext API ensures that the component will stay in sync with the object being provided and the value of screenSize.

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

Pros

Prevents prop drillingless coupling between parent and child components

Promotes one way data flow

Cons

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 Context sparingly

Disclaimer

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.

Instead:

  1. Break your code into smaller components
  2. Keep data flowing one way
  3. Focus on the ‘Single Responsibility’ rule
  4. Keep it simple

Conclusion

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.

Article originally posted by Adam Hannigan on Medium.com