<img height="1" src="https://www.facebook.com/tr?id=&quot;1413357358800774&quot;&amp;ev=PageView&amp;noscript=1" style="display:none" width="1">

Design patterns were created to standardize solutions to the most common problems encountered by developers in applications of all types. They provide proven and transparent approaches to many important issues in projects. Using them dramatically reduces the number of errors that can occur during application development and makes code easier to read by other colleagues, making work more enjoyable and faster. Most patterns are available regardless of the language, it is only important that the paradigm is appropriate. There will also be unique ones, the use of which is limited to the application layer (frontend and backend), or the framework used in the project. In this article I will introduce examples of patterns used in applications made in the React library.

Table of Contents:

    1. Optimizing State Management in React - Context Pattern
    2. Enhancing Reactivity in Applications - Leveraging the Observer Pattern
    3. Container-presentation pattern
    4. Compound pattern
    5. Render props pattern
    6. Higher-Order Components pattern
    7. Hooks pattern
    8. Summary

Optimizing State Management in React - Context Pattern

Using context in React mainly nullifies the drawbacks of the pattern called Singleton, which involves implementing a single instance of an object that can be accessed from anywhere in the application. Singleton is still used in applications, such as for state management using the Redux library. Unfortunately, it is referred to as an anti-pattern due to its global access to objects, which makes it difficult to debug the code if a problem occurs. 

Context provides us with the creation of scopes based on a component tree that restricts access to objects, so it is similar to Singleton, yet due to this important recommendation it is definitely more often recommended and used. It is also worth mentioning that improper or excessive use of multiple contexts can cause problems with component rerendering, so this pattern should be used with caution and often based on good practices to avoid unnecessary rerendering. 

Below is an example of using a context with state, along with the ability to change it inside nested components:

First, you need to create a new, empty context, in this case, according to established standards, it is in a separate file from the components that use it.

The main component of the application has a state, which gets set as the value of the context. To allow nested components to access the context, simply wrap them in Provider. Then inside them you can use Consumer, or hook useContext in the case of modern solutions.

The Greeting component returns a greeting to the user on the screen based on the data set in the state. Here it uses the useContext hook to extract data from the context. It is possible to send this data in props due to the low nesting of the component, but for the purpose of the example, UserContext was used here.

Another component used in the application contains a simple form where the user can change the data stored in the state. In this case, it also uses a hook to operate on data from the context.

For such low-nested components, a better choice would be to simply use the component's properties. However, when the component is, for example, at the eighth degree of nesting relative to its parent, wanting to avoid prop-drilling the context undoubtedly becomes a better option.

Enhancing Reactivity in Applications -  Leveraging the Observer Pattern

This involves subscribing to an element marked as observable by one or more observers. Such an action is designed to manage the content sent to observers through a single intermediary. This pattern is ideal for sending all types of notifications. It is almost arbitrary to subscribe more observers to the intermediary, as well as unsubscribe them as needed. Calling a method to perform a certain action on the observable object will cause it to be executed for each currently plugged-in element. Using the pattern can be very useful when working with asynchronous data transfer. A popular library that provides the above features can be RxJS. It is necessary to use observers with caution, as a large number of them hooked up at the same time when an action is called can cause worse application performance.

Below is an example of using the pattern on the example of a notification of a user clicking on a button:

At first, you need to create an observable element, in this case a simple class containing observers elements, and a few methods to operate on them.

The app renders a button, the pressing of which triggers the execution of the publish action in the Observable object, which is assigned to display the native alert. Assigning the action with the subscribe method is done in useEffect right after the App component is rendered. Naturally, it is also useful to unsubscribe when the component is to be unmounted.

This is a simple example to demonstrate the principles of this pattern. Of course, other use-cases can be much more complex than the use with notifications, an example of which is the MobX state management library, which has the observer pattern combined with a singleton.

Container-presentation pattern

The main purpose of this pattern is to separate the components in which the retrieval or processing of data takes place from those that are used solely to present it to the user on the screen. Presentational components deal with the way information is displayed on the screen; they mostly do not have their own state, as they receive data sent from above.

Container components are used to separate the logic used to retrieve data and then send it downstream. They should have no other components than presentational components, their existence is purely logical. Components divided in this way can be easily reused, the code itself has a predictable and clear structure, as well as testing the application becomes easier. As an example, I will present components for retrieving and displaying user posts according to the pattern described above:

The application renders only one component this time, which is responsible for the role of the container in this example.

The component responsible for the container role fetches data from the specified endpoint, updates its state based on it, and then maps over the array and displays the presentational type components.

The component responsible for the presentational role does not have its own state, it is only used to display the data passed by the properties.

Compound pattern

It is often required to create several components for the operation of a single functionality. This is where the compound pattern comes to the rescue. The main idea of this pattern is to create a component that stores minor components, from which the functionality will be implemented later. The main component holds the state in itself and sends it to the smaller components via the context. Such a procedure results in much less code to write and a logical and semantic connection between the components of the feature being built. The pattern works well when building a component library.

The Compound pattern will be demonstrated using the example of components: wrapper, buttons and graphics, which together will form a simple gallery, and the context required for data operations:

Above, a basic empty context was created, which will be used in the pattern example.

The component responsible for the container for the gallery has been hooked up to the context along with the state, which will be used in the component parts of the functionality. The components have also been hooked up to the object holding the main component.

The first component is responsible for displaying the photo based on data from the context.

The second is a button, which can be used to change the currently displayed image from the data received from the context. Based on the type property (next or previous), the action that is performed when the button is pressed is selected.

The main component of the application renders the gallery based on the pattern described above. All components used are semantically connected to each other and used only to build the gallery. The code is clean and all logic is encapsulated in the gallery module.

As you can see from the above example, this pattern is perfect for segregating the components responsible for one feature, makes it easier to manage the state by using context, and definitely improves the readability of the code.

Render props pattern

This pattern involves passing in the properties of the component of the second component to be rendered inside the structure of the former. Technically, prop children is one of the most commonly used render props in React. It is another method that allows you to share logic across multiple parts of your application. Since the advent of hooks in React, render props are used less and less, but it is worth remembering the existence and use of this pattern.

An example of the use of a render property can be a component responsible for displaying a modal. Mostly modals look very similar. They have the same header, but the main content and buttons are different. This is a good example of the use of the pattern described above:

To begin with, the code shows the basic form of the Modal component. Its content is sent via rendering properties. The component is just a wrapper for the uploaded elements.

The code of the application's main component has two methods used to handle buttons, as well as the state based on which it renders the modal. To use the rendering property, all you have to do is upload a JSX element in the component property in question.

Using this pattern to create reusable components as seen above is very simple and transparent. It helps to avoid repetition, keep the code in the repository clean and maintainable, and most importantly, make your work easier.

Higher-Order Components pattern

One method to share the same logic across multiple parts of an application is higher-order components. These components can be thought of as a wrapper for the component in which the shared logic is embedded. These are methods that take a component as an argument and then return it back richer with new logic. Using Higher-Order Components reduces the risk associated with bugs in new code fragments, helps maintain the DRY (Don't Repeat Yourself) principle at work, and the cleanliness of project code. In many cases, hooks can be used instead of higher-level components. This will certainly reduce the amount of nesting occurring in the component tree, which can be cumbersome if the technique is abused.

An example of using a higher-order component would be to create a logic and loader component with the ability to use it in any component, without interfering with its code:

The withLoader component takes as a parameter another component and the URL from which it retrieves data. When the data is retrieved, the component is displayed, otherwise the Loader appears on the screen.

The Posts component was passed to a higher-level component before export, so using it in a new location will already contain the logic and elements of the withLoader component.

For the sake of formality, a representation of how the Posts component is rendered in the main application component has been provided.

By using higher-order components, we can isolate repetitive elements or logic and thus improve code readability, and allow easy implementation of changes to shared elements. Nowadays, higher-order components are used much less frequently, mainly due to the introduction of hooks into React.

Hooks pattern

This is another method for sharing logic between components. Hooks were introduced to React with version 16.8. Since then, it is no longer required to create class components to enable state operations or use component life cycles. Using the modern approach of sharing logic in code that is hooks, it is possible to replace them by above-described patterns of Higher-Order Components, Render Props or using a hook as a presentational component, among others. It is worth noting how many possibilities are provided by writing your own hooks, in which it is possible to combine many other hooks built into libraries. Using hooks helps keep your code tidy, thanks to less code compared to class components, creating stateless components where logic can be encapsulated in the body of the hook, or above all, thanks to the ability to share logic.

Due to the large number of built-in hooks in React, I will only present the practice of writing your own functionality. An inventory of all the hooks available in the library can be found here: https://react.dev/reference/react. The first example will be to use a custom hook to handle events:

The hook is used to capture a click outside the element to which the reference is passed as a parameter. After detecting the click, the hook executes the function, also sent in the function parameters. This functionality is useful and often used, for example, in modals, dropdowns, or popups.

Another example would be to improve the performance of the component responsible for the container in the Container-Presentational pattern described above: https://www.patterns.dev/react

To achieve such a result, a slight reworking of the main component of the application is required to replace the container component with a hook.

The Post component itself remains unchanged, and all data retrieval logic is encapsulated in the usePosts hookup described below.

The achieved effect is very close to the previous approach, but differs in that it does not require rendering another redundant component, but encapsulates the logic in a hook.

The last example of use could be to replace a Higher-Order Component with a custom hook, here I will also use the example from the pattern described above:

Here the operation has been slightly changed, because the hook returns the loader component and the data to be displayed on the screen. Therefore, a change in the Posts component must also take place in this example.

The component has been reworked to make it work in accordance with what the hook returns. The screen will display the same thing as in the example using a higher-level component. Both patterns are good choices worth considering, and it cannot be clearly stated that one is better than the other.

As you can see from the examples above, writing custom hooks is a great choice when it comes to writing shared logic for functionality in your application. It's important to remember that you can only use them inside function components. There are many libraries that add many different and useful hooks, an example is the usehooks-ts package. A list of all the functions added by the library can be found at the link: https://usehooks-ts.com.

Summary

Using the above-described design patterns will undoubtedly make the work on the frontend layer of the project easier. It takes time to implement the right patterns, it is difficult to predict the behavior of the various functionalities of the application at the beginning of the work. Scalability of the project will then not be a problem, and the code itself will be clean, predictable and better understood by other developers. The process of working on the application itself will be much more pleasant for each department involved in the project, which should translate into productivity during the project, resulting in a lower cost of execution and at the same time greater profit.

Bear in mind that sustaining an efficient web application is a continuous endeavor. As you introduce fresh functionalities and witness an expansion in your user community, it becomes imperative to consistently oversee and refine your application. Embracing the tactics elucidated in this piece and maintaining a vigilant approach towards performance enhancement will guarantee a seamless, streamlined, and captivating experience for your users when interacting with your web application.

check out our case studies