Introduction
JazzTeam is pleased to present you a compilation of its experience in the field of front-end development. In particular, the experience of using OOP in React. With its pros and cons.
Such expertise allows JazzTeam specialists to implement architecturally competent software solutions, simplify further support, and minimize technical debt.
React is one of the most popular libraries for web application development. In this article, we will consider object-oriented programming in the context of React and discuss how to apply it in different approaches to component writing. We will also focus on the concept of classes in JavaScript, how they are implemented under the hood, and analyze what differs them from classes in Java and TypeScript. We will consider two main ways to create components in React: functional and class approaches. Using examples, we will analyze how to apply the concepts of OOP in both approaches. Each of them has its own advantages, and applying OOP principles can improve the code structure and readability. One of the most interesting aspects of OOP in React is the possibility to create classes to represent domain entities and further integrate them into functional components. We will consider how this can improve the app structure. In addition, we will consider (with code examples) how and which OOP patterns can be successfully applied in the functional approach to the development of React applications.
1. What is object-oriented programming in React?
Object-oriented programming (OOP) is a programming methodology in which code is structured around objects. These objects are encapsulated data structures that contain properties (variables) and methods (functions) and can manipulate this data. The main idea of OOP is to facilitate code organization, ensure its readability, as well as make the process of supporting and expanding the software product easier.
React is a JavaScript library designed to develop user interfaces. Despite the fact that JavaScript itself is a prototype based programming language and does not have a classical OOP model inherent in, for example, Java or C++, React uses OOP concepts.
Instead of classes and inheritance typical for conventional object-oriented languages, these concepts are implemented through the concept of components in modern React syntax. Components are reusable code blocks that include both data (state) and methods (functions) to manage that state. In this library, components can be considered as objects that have properties and the ability to perform certain operations. However, this was not always the case. When React was first introduced to Facebook, it used class components. This was the approach many developers were accustomed to, as they were familiar with classes and objects from the OOP world. In class components, the state and logic of the component were encapsulated within the class. Class components are still supported by React and can be used in projects created earlier or when needed.
It is important to note that classes in Java and JavaScript share some common concepts such as encapsulation, inheritance, and polymorphism, but they also have significant differences due to different approaches and purposes of these two languages. Let’s study in detail the differences between classes in Java and JavaScript.
Table 1 – Comparison of classes in Java and JavaScript:
So, it can be seen that classes in Java and JavaScript have some similarities, such as the possibility to describe objects and their behavior. However, they also have significant differences.
The TypeScript (TS) language is more closely related to Java, which is a superset of the JavaScript language. It provides additional tools and functionality for developing complex JavaScript applications. The main feature of TypeScript is static typing, which allows defining the data types of variables, functions, and objects at the compilation stage, which helps prevent many common errors in JavaScript. That is why Java and TS may seem similar at first glance. But it’s important to note that Java is always statically typed, while TypeScript is a statically typed add-on for JavaScript, which means you can use dynamic typing if necessary.
For clarity, the key similarities and differences of classes in Java, JS and TS are briefly indicated in Table 2 “Comparison of classes in Java, JS and TS”
Table 2 – Comparison of classes in Java, JS and TS:
The conclusion that can be drawn from the comparisons that were made in this section is as follows: ES6+ made many changes to JavaScript and brought it closer to Java and TypeScript in terms of syntax and class capabilities. However, TypeScript adds additional static typing and other features, which makes it more powerful for developing large projects. It can also be said that React supports and uses OOP concepts to create modular, reusable, and easily supported components. These concepts will be discussed in detail below.
2. Using OOP in two ways of writing components in the React library (functional and class approaches)
It should be noted that in React 16.8, class components receded into the background compared to functional components. Functional components become more concise resulting in cleaner and simpler code. Everything that can be implemented using class components can also be implemented using functional ones. Except for a special class component called Error Boundaries, which cannot be duplicated as a functional component because it is implemented using the componentDidCatch lifecycle method that has no alternative in functional components.
Since class components are inherited from React.Component, state and lifecycle methods are associated with them. Their presence requires a better understanding of when lifecycle events occur and how to respond to them for state management. Classes also require additional component configuration or execution of API calls for data that is primarily implemented through the constructor. Sharing logic across multiple class objects without implementing design patterns is a more challenging task, which results in the increasing code complexity and difficult maintenance.
Deciding which component to use always comes down to discussing inheritance and composition. Functional components encourage the use of composition, while class components lend themselves to inheritance design patterns.
Currently, composition is considered a best practice in programming, so most new React projects use functional components instead of class components. However, React still supports class components for outdated goals.
It is important to remember that the functional style in React and the class style are not opposite, they simply provide different tools for solving the same tasks.
2.1 Implementation of OOP principles in the class approach
Before React 16.8, the main way to create components was the class component, which used JavaScript classes to define components. In this approach, components are considered as classes that can have states, methods, and life cycles. The class approach in React does encapsulate some basic object-oriented programming concepts and provides some of their benefits. Here are the aspects of OOP that it includes:
- Encapsulation:
Encapsulation in OOP means hiding the details of object implementation and providing an interface to work with it. Encapsulation allows developers to create more robust and supported components because state and functionality management is limited within the component, and external code can interact with the component only through explicitly defined interfaces (methods for class-based components).
Code Example 1 – Standard Encapsulation Example in the Class Approach in React:
The count state and increment method are encapsulated here, and their implementation details are hidden from the outside world.
Let’s consider a non-standard example of encapsulation in React using static methods. Static methods belong to the class itself, not its instances, and they can be used to organize functionality that is independent of a particular component instance.
Code Example 2 – Implementation of Encapsulation Using Static Methods in the Class Approach in React:
In this example, MathOperations is a component that contains static methods for performing addition, subtraction, multiplication, and division operations. We can call these methods without instantiating the component, which demonstrates the encapsulation of functionality within the class. Then, inside the render() method, we use these static methods to perform mathematical operations and display the results on the screen. The advantages of this approach are that the functionality remains encapsulated within the component, and it can be used in other parts of the application without creating an instance of the component.
- Inheritance:
Inheritance in OOP allows creating new classes based on existing ones and inheriting their properties and methods.Applying the class approach in React, you can create successor components by extending the functionality of basic components. But this approach is used less frequently in React than the components composition. (We will consider the example together with polymorphism and abstraction)
- Polymorphism:
Polymorphism can be implemented by transferring various properties to the components. Different components use the same properties for different purposes. (We will consider the example together with inheritance and abstraction)
- Abstraction:
The class approach allows creating abstract components that define common methods and properties for multiple components, which facilitates code reuse.
Code Example 3 – Implementing Inheritance, Polymorphism, and Abstraction in Class Approach in React:
In this example, we have the Media base class. It represents an abstract component that defines a common interface in the form of the renderMedia() method. This method shall be overridden in inherited classes. Then there are three inherited classes: Image, Video, and Audio, each of which overrides the renderMedia() method to display the corresponding media files. Abstraction allows defining a common template for the components and ensuring their consistent behavior, while each specific component can be configured to display unique content. And each of the Image, Video, and Audio components is polymorphic because they inherit from Media and represent different behavior overriding the renderMedia() method. This allows us to easily add new types of media files without changing the code of the App component.
- Single Responsibility Principle (SRP):
Each class should have only one responsibility. In React, this means that components should have only one responsibility.
Code Example 4 – Implementing the Single Responsibility Principle in the Class Approach in React:
In this example, the Counter component is limited to performing tasks related to displaying, zooming in, and resetting the counter. Its area of responsibility is limited to these tasks and does not include information about what exactly is counted. Responsibility for a specific count is delegated to the App component located higher in the hierarchy. The App component, in turn, combines all other components, displays them and establishes a link between each Counter instance and a specific vegetable. Following the Single Responsibility Principle, you can greatly simplify the process of expanding the application.
- Open/Closed Principle (OCP):
Software entities such as classes, modules, and functions should be open for extension but closed for modification. This principle is also applicable in the class approach in React.
In the class approach, this principle is achieved by implementing inheritance. It is necessary to create a base class (or component) with general functionality and then expand it by creating subclasses with additional functionality. This allows adding new functionality without changing the source code of the base class. This principle is often used in the class approach.
- Liskov Substitution Principle (LSP):
If there is a base class and a class derived from it, then instances of the derived class should safely replace instances of the base class without disrupting program running.
As for the class approach in React, this means that components in React inherited from each other must meet certain expectations and interfaces so that one component can be safely replaced with another. The LSP principle helps to create flexible and extensible code allowing to safely replace base class objects with derived class objects. This is especially important in React, where components are often inherited and extended to create more complex interfaces and functionality.
Code Example 5 – Implementing Liskov Substitution Principle in the Class Approach in React:
The Liskov Substitution Principle ensures that derived classes (Circle and Rectangle) can safely replace the base class (Shape) in any context where the Shape object is expected and this will not cause unwanted program failures.
- Interface Segregation Principle (ISP):
In the context of the class approach in React, the Interface Segregation Principle implies that components should provide only those methods and properties that are really necessary for their correct operation, and not impose unnecessary functionality. This principle is applied continuously.
- Dependency Inversion Principle (DIP):
High-level modules (classes, components) should not depend on low-level modules (specific implementations), both should depend on abstractions. In React, properties are often used to pass dependencies to components. This allows components to be independent of specific implementations, as they simply expect to be given the necessary data and functions through properties (in the Abstraction principle we considered the example using properties). This principle can also be implemented using Higher Order Components (HOC). Higher Order Components provide common logic and functionality that can be used by multiple components. This allows the control to be inverted, as higher order components determine how and when to call component methods.
Code Example 6 – Implementation of the Dependency Inversion Principle Using HOC:
This approach allows easily changing the data source or adding other dependencies without changing the UserList code. This is in line with the Dependency Inversion Principle and makes the code more flexible and testable.
2.2 Implementation of OOP principles in the functional approach
The functional approach in React can also implement OOP principles, but uses more functional concepts and tools to achieve the same goals.
Examples of OOP principles implementation in the functional approach in React:
- Encapsulation:
There are no classes and methods in the functional components compared to the class components, but it is possible to use hooks to encapsulate data.
Code Example 7 – Implementing Encapsulation in the Functional Approach in React:
In this example, we used the useState hook to encapsulate the counter state. We also created the increment and decrement functions that encapsulate the counter state change logic. So, the data (counter state) and the logic (increment and decrement functions) are encapsulated within the Counter functional component.
- Inheritance:
Inheritance is unavailable in React functional components compared to the class components. Instead of classes and inheritance, other approaches are used, such as component composition, props inheritance, and hooks.
- Abstraction:
In the functional approach in React, abstraction is often achieved by creating functions or components that encapsulate certain logic or behavior to simplify the code and make it more readable and maintainable.
Creating custom hooks can be an example. We can create custom hooks that encapsulate complex behavior and then use those hooks in different components extending their functionality without changing the components themselves.
Code Example 8 – Implementing Abstraction in the Functional Approach in React:
- Polymorphism:
Polymorphism in functional React is achieved by creating components or functions that can take different types of input data (props) and behave differently depending on this data. This allows creating flexible and reusable components that can adapt to different use cases.
- Single Responsibility Principle (SRP):
In the functional approach, we strive to ensure that each functional component is responsible for only one specific part of the interface or functionality. This can be achieved by dividing the components into smaller, atomic components, each of which is responsible for a specific task.
Code Example 9 – Implementing the Single Responsibility Principle in the Functional Approach in React:
This example shows the UserNameInput and UserEmailInput components, each of which is responsible for its own input element. The UserForm component deals only with the composition of these components and the processing of form events.
The use of the Single Responsibility Principle helps to create a more modular and easily maintained code in the functional approach in React.
- Open/Closed Principle (OCP):
The Open/Closed Principle obliges us to structure our components in such a way that they can be expanded without changing the source code. Let’s consider the example below. We need to implement the SpecialInformation component in the application, which is used on different pages of the application and its content differs depending on the location.
Code Example 10 – Violation of the Open/Closed Principle in the Functional Approach:
You can see that in this implementation there is the following situation. When adding new pages and the SpecialInformation component to each of them, we have to reconfigure the SpecialInformation component. This approach makes our component contextual and contradicts the Open/Closed Principle.
Code Example 11 – Implementing the Open/Closed Principle in the Functional Approach:
Using the principle of composition, we completely removed the logic that was contained in the SpecialInformation component, and now we can transmit any information without changing the component itself.
By following the Open/Closed Principle, we can reduce the connection between the components and make them more extensible and reusable.
- Liskov Substitution Principle (LSP):
In the functional approach in React, components can be interchangeable if they follow a common interface. Functional components can have the same signature (props and state), which allows replacing some components with others without disrupting the application operation.
Code Example 12 – Implementing Liskov Substitution Principle in the Functional Approach:
In this example, two functional components, BaseComponent and ExtendedComponent, take the same text property and render it with some additional information. The renderComponent function takes the component as an argument and renders it. In this case, we transfer both the base component and the extended component, and both are successfully rendered.
According to the Liskov Substitution Principle, the ExtendedComponent functional component here is a BaseComponent subtype, and we can replace BaseComponent with ExtendedComponent without changing the program behavior. This ensures the predictability and correct operation of our code in accordance with the Liskov Substitution Principle.
- Interface Segregation Principle (ISP):
Functional components make it easy to segregate interfaces into smaller components. This allows creating components which provide only those methods that are really necessary to interact with them.
So, the Interface Segregation Principle reformulated for React may sound like this: “Components should not depend on props that they do not use.” Suppose we have a UserInfo component that displays user information.
Code Example 13 – Violation of the Interface Segregation Principle in the Functional Approach:
You may note that the UserInfo component accepts the User property: the object with all user data, but not everything is used in the component. Instead of transferring a large userInfo object with all possible user data, we will transfer only the necessary props:
Code Example 14 – Implementation of the Interface Segregation Principle in the Functional Approach:
In the good example, the UserInfo component depends only on the necessary name, age, and email props, not on the entire user object. This avoids unnecessary dependencies and makes the code cleaner and more maintainable.
- Dependency Inversion Principle (DIP):
The Dependency Inversion Principle in React can be formulated as follows: high-level components do not depend on low-level components, but both depend on abstraction. Let’s consider the example we already know based on the Interface Segregation Principle.
Code Example 15 – Implementation of the Dependency Inversion Principle (props) in the Functional Approach:
In this example, the App component is a high-level component and UserInfo is a low-level component. The abstraction is the userInfo object. It turns out that App depends on userInfo, since the data is necessary for transmission to the UserInfo component. Data implementation, its rendering, takes place already within the UserInfo component, which also depends on userInfo.
The mechanism for transmitting data through props is not the only example where the Dependency Inversion Principle is implemented. Let’s consider another example.
Code Example 16 – Implementation of the Dependency Inversion Principle in the Functional Approach:
In this example, ProductService is responsible for retrieving product data, and PriceFormatter is responsible for price formatting. The ProductInfo component uses both abstractions to display product information, including the formatted price.
The Dependency Inversion Principle is applied, since ProductInfo does not directly depend on specific implementations of data acquisition and price formatting. Instead, it depends on the ProductService and PriceFormatter abstractions, which makes the component more protected from changes in these aspects.
This approach simplifies replacing or modifying the data acquisition and price formatting logic without requiring a change to the ProductInfo component.
The principles of object-oriented programming and functional programming can be successfully combined when developing in React. It is important to understand that these principles will not be implemented in pure form, but will be modified for functional concepts and tools.
2.3 Functional approach and creation of classes for domain entities
Using functional components in React in combination with classes for domain entities is a fairly common and flexible approach that allows using the best of the functional and object-oriented approaches.
The functional components in React are well-suited for creating a user interface. They are easy to read, can use hooks to manage the state and lifecycle of a component, and provide a more concise and declarative syntax. However, classes may be more convenient for domain entities that have business logic and methods. You can define methods and properties in specific classes to handle the logic associated with these entities. In addition, inheritance and other OOP concepts can be used to model domain objects better.
By dividing the logic of the domain area into classes, we implement a clear division of tasks within our project. The functional components will focus on rendering and user experience while remaining concise. This readability simplifies the understanding of component logic and helps developers determine the purpose and functionality of each component. Domain entity classes handle business rules and manipulate data.
Well-designed entity classes can be reused throughout the application. The ability to reuse reduces code duplication and ensures consistency between different parts of the application.
Large applications often require the ability to scale and implement new features. The functional approach, combined with domain entity classes, provides a framework that can be scaled depending on the complexity of the application. If necessary, it will be possible to add new classes and components without significantly affecting the existing code.
Code Example 17 – Simple Example of Implementing the Creation of a Class for a Domain Entity in the Functional Approach:
It is important to note that this example is used solely to illustrate the syntax of the functional approach using classes for domain entities. In practice, this approach is widely used to effectively solve complex problems and often finds its application in large projects.
3. Implementing OOP patterns in the functional approach in React
Object-oriented programming patterns can also be implemented in a functional style. Implementing these patterns in a functional style can be more flexible and combined with the benefits of functional programming. Here’s how some of them can be implemented:
- Observer Pattern:
This pattern allows objects (observers) to be automatically notified of changes in the object they are observing. This pattern is implemented in the functional React using the UseState hook. Using the useState hook, we can add and manage the state in functional components.
- Decorator Pattern:
The decorator adds functionality to the object without changing its structure. In the functional approach, this can be achieved by wrapping the components into other components (Higher-Order Components, HOC).
Code Example 18 – Example of Implementing the Decorator Pattern:
- Proxy Pattern:
The Proxy pattern allows separating the basic logic of the components from additional functionality and optimizations, which helps to improve the performance and maintainability of the code. The Proxy pattern in React can be implemented using a variety of techniques to add additional functionality or optimize component performance. You can use React.memo to create proxy components that memoize their properties and prevent additional rendering if the properties have not changed. You can use lazy component loading with React.lazy to load a component only when you really need it. Or you can write your own proxy component that implements additional logic before displaying the original component.
Code Example 19 – Example of implementing the Proxy pattern:
- Strategy Pattern:
The strategy allows choosing from several behavior algorithms. In a functional style, this can be implemented by transferring callback functions as props.
Code Example 20 – Example of Implementing the Strategy Pattern:
- Composite Pattern:
This pattern allows creating tree-like structures of objects. In a functional style, this can be implemented using recursive components.
Code Example 21 – Example of Implementing the Composite Pattern:
- Mediator Pattern:
The Mediator can coordinate the interaction between the components isolating them from direct interaction with each other. It can be implemented using hooks to control interaction between components. Let’s consider an example where the Mediator sets the playback status and volume level in the app.
Code Example 22 – Mediator Pattern Implementation Example:
- Facade Pattern:
It provides the user with a simple and standardized way to communicate with a complex system, while abstracting it from the details of the execution of each part of the system and the internal relationships between them.
In the context of the functional approach and React, we can consider the Facade pattern as a component that encapsulates complex logic or interaction with other components providing a simpler and more intuitive interface for other components.
Code Example 23 – Facade Pattern Implementation Example:
The use of OOP patterns in functional React is not mandatory, but can be useful. It is important to approach this issue flexibly and adapt the choice of patterns to the context of a particular project.
Conclusion
Object-oriented programming plays an important role in the development of web applications in React. In this article, we studied various aspects of OOP related to this library and showed examples of applying OOP in different approaches to writing components. It has been proven that the principles of object-oriented programming and functional programming can be successfully combined when developing in React. It was also noted that ES6+ made many changes to JavaScript and brought it closer to Java and TypeScript in terms of syntax and class capabilities.
Particular attention was paid to the possibility of creating classes for domain entities in the functional approach. This approach allows using the best of the functional and object-oriented approaches, helps manage the state and logic of the application, and also provides a clear division of responsibility. In addition, we provided code examples showing how and which OOP patterns can be successfully applied in the functional approach to the development of React applications.
As a result, knowledge of the principles, patterns of object-oriented programming and their application in React can significantly improve the quality and maintainability of code making web application development more efficient and convenient.