Creating Highly Maintainable React Applications With Composable Components
In recent years, the web has evolved significantly, leading to the creation of more complex and dynamic applications and websites. With this evolution, developers face the challenge of creating highly maintainable and scalable React applications. As projects grow in complexity, so does the challenge of maintaining application performance and clarity. However, a powerful solution lies in the use of composable components.
React composability allows developers to build modular and reusable components, thereby enhancing both the performance and maintainability of applications. In this article, we will discuss in detail how you can leverage composable components to create scalable and efficient React apps.
The Fundamentals Of React Composability
At the heart of every React application are components. Components are reusable blocks of code that contain both logic and UI elements. They serve similar purposes as functions in JavaScript and normally return HTML. Components are often combined to create complex UIs. In React, components are of two types: functional components and class components.
Functional Vs Class Component
To properly illustrate and differentiate a functional component from a class component, we will build a simple info card as shown in the image below, using both component types.
- Functional component
Functional components are basic JavaScript functions that take in props
as an argument and return React elements (JSX). They are just like functions in JavaScript. however, instead of holding some complex javascript codes, they return JSX.
It is important to note that functional components are the backbone of most modern web applications built with React.
Syntax:
function Card() {
return (
<div>
</div>
)
}
Now that we’ve seen what a basic React functional component looks like, it is time to apply it in creating a simple info card.
To do this, we start by creating our react project using:
npm create-react-app my-first-react-app
Next, we change the directory and enter our React folder using the command below:
cd my-first-react-app
Having done all this, it's time to create our info card.
import React from "react";
import CardOne from "./CardOne";
import "./Card.css";
function Card() {
return (
<div className="card">
<div className="image"></div>
<div className="description">
<p>Africa</p>
</div>
<div className="content">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui excepturi
quo, officiis fugiat illo perspiciatis veniam dignissimos, soluta
dolor magni blanditiis. Eos at facilis reiciendis quibusdam aperiam
fuga consectetur consequuntur?{" "}
</p>
</div>
</div>
);
}
export default Card;
The code above contains a simple functional component called Card
, which is designed to return a set of JSX. Within this component is a div
tag with a class name of 'card'
acting as the parent div. This div
tag holds three other divs
with class names: image
, description
, and content
, acting as child divs. The image div
contains the image of our info card (which is added in our CSS file), the description div
holds a p
tag that describes what our info card is all about, and the content div
contains another p
tag that holds the info card content.
To display this in our browser, we need to call our card component within the app component, which acts as the parent component.
import React from "react";
import Card from "./components/Navbar/Card";
function App() {
return (
<div>
<Card />
</div>
);
}
export default App;
Output:
The above image shows our info card without the necessary styles added.
To make the info card look better, we apply the necessary CSS styles.
* {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.card {
width: 400px;
height: 600px;
margin: auto;
margin-top: 70px;
background: #141414;
border: solid black 1px;
}
.image {
background-image: url(./1000_F_627731195_osJSHAWmrNeBWi7RSm8rbXNbBVSVFE3k.jpg);
background-size: 400px 400px;
background-repeat: no-repeat;
width: 100%;
height: 400px;
margin: auto;
}
.description p {
width: 60px;
height: 30px;
border-radius: 14px;
text-align: center;
padding: 5px 2px;
margin-left: 15px;
background: rgb(158, 34, 34);
font-weight: lighter;
color: #d3d3d3;
}
.content p {
color: #d3d3d3;
margin: 0px 15px;
text-align: justify;
font-weight: lighter;
}
From the code above, we start by applying a general style to our info card using the '*'
selector. We set the box-sizing
property to border-box
and choose our font. Then, we style the parent div with the class name "card."
We set its width
and height
to 400px
and 600px
, and its background
to black. We also set the margin
and border
properties.
Next, we style the child div with the class name "image."
We use the background-image
property to define our image and make sure it fits within the parent div using the background-size
, width
, and height
properties.
Additionally, we apply specific styles to the "description"
and "content"
divs, focusing on the p
tags within them.
Output:
The image above shows an info card created using a functional component.
- Class component
class components are called stateful components as they contain both state and lifecycle methods. They are more complex than their functional counterparts because they can manage their state and lifecycle. Class components are written with JavaScript ES6 classes, and unlike functional components, the render method is responsible for returning their JSX elements.
A basic syntax of a class component is shown below.
Syntax:
class card extends React.Component{
render(){
return(
<div>
</div>
)
}
}
creating an info card with class component is almost similar to the above info card created using functional component except the class component syntax is utilized. Below is a code example:
import React from "react";
import "./Card.css";
class Card extends React.Component {
render() {
return (
<div className="card">
<div className="image"></div>
<div className="des">
<p>Africa</p>
</div>
<div className="content">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui
excepturi quo, officiis fugiat illo perspiciatis veniam dignissimos,
soluta dolor magni blanditiis. Eos at facilis reiciendis quibusdam
aperiam fuga consectetur consequuntur?
</p>
</div>
</div>
);
}
}
The above code shows our info card structure set up within a class component. The render method, which was absent in our functional component code helps our class component render our JSX elements to the Dom.
The Role Of Props And State
In building composable components, understanding props and state is crucial for creating modular and maintainable React applications.
- Props
Props
(short for properties) are special parameters in React that are used to pass data from one component to another. The flow of data from one component to another is unidirectional, meaning data can only be passed from a parent to a child component via props
. However, child components can also communicate with parent components through call back functions passed down as props
. This is to ensure a clear and predictable data flow.
To fully understand how props
work in React, we will be creating a simple sign-up form where form properties are passed from the parent to the child component.
For our sign-up form, we will be creating two component. the first named property
which will serve as the child component and hold the input
field with props
, and the second named app
which will serve as the parent component and where the child component will be rendered.
Code for child component:
import React from "react";
import "./Property.css";
import { render } from "react-dom";
class Property extends React.Component {
render() {
return <input type={this.props.Type} placeholder={this.props.Name} />;
}
}
export default Property;
The code above contains our child component named Property
. Inside this component is an input
element with two attributes
: type
and placeholder
. Instead of assigning specific values to these attributes, we use props
with the this.props
method. For the type
attribute, we name our prop
"Type,"
and for the placeholder
attribute, we name our prop
"Name."
Now that we've done this, it's time to use the Property component in the parent (App) component and define the Type
and Name
props there.
Code for parent component:
import React from "react";
import Property from "./components/Navbar/Property";
class App extends React.Component {
render() {
return (
<div className="form">
<p>Sign Up</p>
<div className="form-1">
<Property Type="text" Name="First Name" />
<Property Type="text" Name="Last Name" />
</div>
<div className="form-2">
<Property Type="e-mail" Name="Email" />
<Property Type="password" Name="Password" />
<Property Type="password" Name="Confirm Password" />
</div>
<div className="form-3">
<Property Type="radio" id="for" />
<label htmlFor="for">
i accept the terms of use & privacy policy
</label>
</div>
<div className="form-4">
<button>Sign Up</button>
</div>
</div>
);
}
}
export default App;
The code above shows our App component acting as the parent to the Property component. Inside this component is a div
tag with a class name of form
. This tag wraps all other tags in our App component, making it the parent div
. Inside the parent div
is a p
tag and four other div
tags with class names: form-1
, form-2
, form-3
, and form-4
.
In our form-1
div
, we call the Property component and set the Type
and Name
props to "text" and "First Name," respectively. This creates a text input field with a placeholder
of "First Name." Still within our form-1
div
, we call the Property component again, setting the Type
and Name
props to "text" and "Last Name," respectively.
Next, we move to the form-2
div
tag, where we call the Property component three times. For the first call, we set the Type
and Name
props to "e-mail" and "Email," respectively. For the second call, we set the Type
and Name
props to "password" for both. For the last call, we set the Type
and Name
props to "password" and "Confirm Password," respectively.
In the form-3
div
, we call the Property component once more and set the Type
prop to "radio," adding a label tag to it.
The form-4
div
simply holds a "Sign Up" button, and our form is all set up.
Output:
To make our sign-up form look a lot better, we need to apply some CSS codes.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
background: rgb(243, 239, 239);
}
.form {
width: 450px;
height: 500px;
border-radius: 10px;
margin: 0px auto;
margin-top: 150px;
background: rgb(55, 75, 83);
box-shadow: 10px 10px 30px rgb(170 164 164);
}
.form p {
text-align: center;
font-size: 30px;
padding-top: 20px;
color: white;
font-weight: 600;
}
.form-1 {
font-size: 20px;
color: black;
padding-top: 20px;
margin: 0 15px;
}
.form-1 input {
width: 200px;
height: 50px;
border-radius: 10px;
padding: 15px 30px;
margin: 0 5px;
}
.form-2 {
font-size: 20px;
color: black;
padding-top: 20px;
margin: 0 20px;
}
.form-2 input {
width: 410px;
height: 50px;
border-radius: 10px;
padding: 15px 30px;
margin-bottom: 20px;
}
.form-3 {
display: flex;
align-items: center;
justify-content: center;
margin: 15px auto;
}
.form-3 label {
font-size: 15px;
color: rgb(76, 175, 80);
}
.form-4 {
margin-left: 80px;
}
.form-4 button {
width: 290px;
height: 50px;
font-weight: 500;
font-size: 15px;
border-radius: 10px;
padding: 15px 30px;
margin: 15px auto;
}
In our code above, we begin by setting some general styles like padding
and margin
using the ‘*’
selector. Next, we set a background-color
for the body of our form.
We also define style properties such as width
, height
, padding
, and border-radius
for the form
, form-1
, form-2
, form-3
, and form-4
divs
.
Output:
The image above displays a beautifully designed sign-up page, making full use of React props
in its creation.
One thing to note is that setting up props
within our Property component, giving each prop
a unique name of "Type"
and "Name,"
and specifying new values for these props each time the child component is called makes setting up our sign-up form a lot easier. Without props, we would have to create so many components, specifying their type
and placeholder
attributes individually, and then calling these components one by one within our App component. That would have been a lot of work, wouldn't it?
- State
Aside from props
, React possesses another built-in object called state
, which allows components to create and manage their data. States are local data storage managed within components. In essence, while props
can be passed from one component to another, states
cannot. States enable components to keep track of changes and re-render the UI accordingly.
It is worth mentioning that creating and using state
involves three steps.
Step 1: Setting Up State
The first step in creating and using state
is setting it up. State
is initialized using the this.state
method, typically placed within React’s constructor method.
Step 2: Updating State
Updating state
is the second step in using state
. States are updated using the this.setState
method, which is usually done within a function or one of React’s lifecycle methods. It's important to note that the this.setState
method triggers the component to re-render with the new state value.
Step 3: Accessing State
Accessing state
is the final step in using state
. States are accessed using the this.state
method.
Now that we’ve covered the basics, let’s demonstrate a practical use of state by building a simple counter app. In this example, the count value will update each time the increment button is clicked.
import React, { Component } from "react";
import "./Counter.css";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div className="counter">
<h1>{this.state.count}</h1>
<button onClick={this.increment}>+</button>
</div>
);
}
}
export default Counter;
The code above illustrates a counter app set within a counter component. Within the counter component is the constructor
method where the initial value of our state
is set using the this.state
method. Note that the state is given the name count
and the initial value is set to zero.
Next in our counter component is the increment function. This function is responsible for updating the state value as it holds the this.setState
method. In this function, our count
value is updated to the initial count
value plus one.
With everything set up, it's now time to access the state
in our counter component. This is done using the this.state.count
method inside the h1
tag.
To improve the appearance of the counter app, we apply some CSS styles.
.counter {
height: 400px;
width: 600px;
border-radius: 30px;
background-color: teal;
margin: auto;
margin-top: 200px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
text-align: center;
}
.counter h1 {
padding-top: 80px;
padding-bottom: 0;
margin-bottom: 2px;
font-size: 120px;
}
.counter button {
width: 40px;
height: 40px;
font-size: 20px;
font-weight: 700;
cursor: pointer;
}
The code above shows certain style properties like width
, height
, margin
, font-weight
, and size
, among others, being applied to the counter app.
Output:
The image above shows a simple counter app where the count value goes up by one each time you click the increment button.
Using Hooks In Functional Components
In the past, state management was exclusive only to class components. This meant that functional components were stateless and only used for presentational purposes. However, with the introduction of hooks in React 16.8, the case became different. Hooks are special functions in React that allow the use of state and other React features in functional components. They enable developers to manage states, side effects, contexts, refs, and more without the need for class components.
There are numerous types of React hooks; however, two of them are primarily responsible for managing states in functional components. They are the useState
hook
and the useReducer
hook
.
- The useState hook:
The useState
hook
is one of the most commonly used React hooks for managing state in functional components. It is a special function that takes the initial state as an argument and returns an array with two elements: the current state value and a function to update the state.
Here is the syntax of the useState
hook:
const [state, setState] =
useState(initialState);
In the code above:
state
represents the current value of the state.setState
is the function that allows the state to be updated.initialState
represents the initial value of the state when the component is rendered for the first time.
- The useReducer hook
The useReducer
hook, like the useState
hook, is used to store and update states in functional components. It is a perfect alternative to the useState
hook when there’s a need to manage complex state logic in functional components. Simply put, the useReducer
hook is useful when there is complex state logic or when a state needs to be updated based on multiple conditions.
Here’s the syntax of the useReducer
hook:
const [state, dispatch] = useReducer(reducer, initialState);
In the code above:
state
represents the current state of the component.dispatch
is the function used to trigger a change instate
. It sends an "action" to the reducer.reducer
is the function that decides how the state should change based on the action received from thedispatch
function.initialState
represents the starting state of the component.
Principles Of Composable Design
Having grasped the fundamentals of React composability, it's now time to understand the principles that guide the creation of highly maintainable and scalable applications. These principles focus on ensuring components are designed for clarity, reusability, and ease of maintenance.
- Single Responsibility Principle
Sitting pretty as one of the principles of composable design is the single responsibility principle. This principle states that components should have one and only one reason to change. This implies that each component should only hold one piece of functionality or logic.
Going by the single responsibility principle, if we have a card component that displays an image, title, and description, then the sole responsibility of this component should be to render these elements and nothing more. By doing so, components become more predictable as their function remains clear-cut, and debugging becomes easier overall.
- Open/Closed Principle
Next in line is the open/closed principle. This principle states that software entities (classes, modules, and functions, among others) should be open for extension but closed for modification. Simply put, in the React context, this principle implies that components should be designed in such a way that they can be extended with new functionality without any alterations to their existing code. This principle often comes in handy in large-scale applications where frequent changes and feature additions are common.
Remember, adhering to this principle as a developer ensures your React components remain robust, maintainable, and scalable.
- Liskov Substitution Principle
The Liskov substitution principle states that objects of a superclass should be replaceable with objects of a subclass without the replacement affecting the correctness of the program. In simpler terms, this means that derived classes must be substitutable with their base classes.
For the React ecosystem, this principle ensures that components remain open to extension and replacement without any possible drawbacks or unexpected behavior. Adhering to the Liskov substitution principle allows for the creation of more flexible and robust React components, ones that can be replaced without the overall behavior of applications being affected, leading to easier maintenance and scalability.
- Interface Segregation Principle
The interface segregation principle states that clients should in no way be forced to depend on interfaces they do not use. In terms of React, this principle simply encourages developers to create specific and focused component interfaces, ensuring that these components do not rely on unnecessary props or methods. By doing so, a cleaner and more maintainable code with improved component reusability is guaranteed.
Advanced Techniques For Component Composition
We have so far established that mastering component composition is a key way of creating flexible and scalable React applications. Now, we will advance our exploration by delving deeper into some advanced techniques that can further enhance component interaction and functionality sharing.
Some of these techniques include:
- Higher-order components
Higher-order components (HOCs) are basic patterns used to reuse component logic. Simply put, HOCs are functions capable of taking a component and returning a new component with improved functionalities. In essence, the higher-order component technique remains a perfect way of injecting additional behavior into components without modifying the entire component structure.
- Render Props
Ever thought of a perfect way of sharing code between React components? Well, the render props technique comes to mind. For simplicity, the render props pattern in React involves sharing code between components using props whose value is a function. This technique comes in handy when injecting custom behavior into components becomes necessary.
- Context API
The Context API in React is a technique that provides developers with a way of passing values between components without necessarily involving props through every level of the component tree. This technique can be quite useful for managing global states like themes, authentication, or even menu visibility.
- Compound Component
Compound components are sophisticated techniques that allow multiple components to work together to produce a more cohesive user interface. Here, the components share states and behavior, providing users with a consistent interface.
Managing States In Composable Components
Understanding the necessary techniques for crafting composable React components is crucial, but effectively managing states within these structures is what sets proficient developers apart from their counterparts. Effective state management in React components ensures not only code reusability but also effective maintainability within a dynamic environment. Below, we outline various strategies required to manage state effectively in composable components, ensuring both functionality and efficiency.
- Local State Management
Local state management refers to data a React component manages and controls all by itself. In React, hooks
, introduced in React 16.8, effectively help functional components manage states
and several other lifecycle features.
The useState
hook
provides a way to track state
in functional components, as it allows the creation and change of state variables, effectively eliminating the need for class components.
A better alternative to the useState
hook
is the useReducer
hook
. This is usually preferred when building complex state logic, when the state value depends largely on its previous value, or when components require optimization.
- Global State Management With Context API
In situations where states need to be accessible by several components across various levels of the application, React's Context API often comes in handy. React Context API is an efficient way of sharing state through the component tree without having to manually pass props through each nested component. This becomes very useful in scenarios where multiple components need access to the same data, such as user authentication status or theme settings.
- Advanced State Management Library
For applications with more complex state management needs, local state management or management with Context API might be insufficient. Therefore, using state management libraries like Redux or MobX becomes crucial. These libraries provide structured approaches to managing state outside component trees, making states easy to track, debug, and manage.
Styling Strategies for Composable Components
Crafting visually appealing and maintainable user interfaces in React applications requires a thoughtful approach to styling, especially when dealing with composable components. As the React ecosystem evolves, so do the techniques for integrating advanced styling solutions. This section explores several such techniques capable of not only enhancing the visual aesthetics of our React applications but also maintaining the flexibility and reusability of our React components.
- Using CSS modules
CSS modules are files in which all class names and animation names are scoped locally by default. This technique ensures that CSS styles are locally applied to components rather than globally implementing styles across an entire application.
The use of CSS modules in styling composable components comes in handy when applying styles to large projects where maintaining consistent styling can become a daunting task, as the approach prevents styles from affecting unintended parts of your application.
- Styled components
Styled components are a popular library in React and other component-based frameworks. They allow developers to write CSS code that styles components within JavaScript files. Styled components operate similarly to CSS modules as their CSS code can only be applied to a single component and is not accessible by other components or elements on the page.
The styled component is designed to utilize the best features of CSS and JavaScript, making it a powerful and dynamic way of styling composable components through template literals.
- CSS and SCSS stylesheets
CSS and SCSS stylesheets are traditional methods for styling web applications that remain widely used in modern React applications. This styling strategy involves creating external stylesheets that are then imported into React components depending on where the style needs to be applied.
Conclusion
We have explored how the principles of composable components create a robust framework for enhancing the maintainability of React applications. From implementing composable designs and leveraging advanced state management tools to integrating styling strategies with CSS modules and styled-components, it’s clear that adopting these practices offers scalability, robustness, and manageability for your React projects. Going forward, remember to integrate these practices into your projects to fully realize their potential and ensure your development process is as efficient and effective as possible.
Subscribe to my newsletter
Read articles from Daniel Efe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Daniel Efe
Daniel Efe
Hello, I'm Daniel. A front-end developer and a professional technical writer. I enjoy documenting my projects and learning new things.