Design patterns are templates for addressing typical software development issues. They are tried-and-true solutions for dealing with the usual problems that React developers face.
New patterns arise as the React API matures, and developers frequently prefer them over previous ones. This guide discusses several important React service patterns for 2024.
React Service Pattern Example
In this example, we’ll build a user data service that retrieves user information from an API and manages state. Then we’ll use this service in a React component.
Step 1: construct a service to retrieve user data and maintain state. We will use the Context API to provide the service to the components.
javascript
// UserService.js import React, { createContext, useContext, useState, useEffect } from 'react'; // Create a context for the user service const UserServiceContext = createContext(); // Define the user service const UserServiceProvider = ({ children }) => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Fetch users from an API const fetchUsers = async () => { try { setLoading(true); const response = await fetch('https://api.example.com/users'); const data = await response.json(); setUsers(data); } catch (err) { setError(err); } finally { setLoading(false); } }; useEffect(() => { fetchUsers(); }, []); return ( <UserServiceContext.Provider value={{ users, loading, error }}> {children} </UserServiceContext.Provider> ); }; // Custom hook to use the user service const useUserService = () => { return useContext(UserServiceContext); }; export { UserServiceProvider, useUserService };
Step 2: Next, we will use the UserServiceProvider to wrap our application and access the user service in a component.
javascript
// App.js import React from 'react'; import { UserServiceProvider } from './UserService'; import UserList from './UserList'; const App = () => { return ( <UserServiceProvider> <UserList /> </UserServiceProvider> ); }; export default App; // UserList.js import React from 'react'; import { useUserService } from './UserService'; const UserList = () => { const { users, loading, error } = useUserService(); if (loading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default UserList;
React Service Pattern Github
Consider the following scenario: we use a service to fetch data from an API.
Step 1: Create a file called dataService.js.
javascript
// dataService.js export const fetchData = async (url) => { try { const response = await fetch(url); const data = await response.json(); return data; } catch (error) { console.error('Error fetching data:', error); throw error; } };
Step 2: Create a component that leverages the service to retrieve data.
javascript
// DataComponent.js import React, { useEffect, useState } from 'react'; import { fetchData } from './dataService'; const DataComponent = ({ url }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const getData = async () => { try { const result = await fetchData(url); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } }; getData(); }, [url]); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return ( <div> <h1>Data</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); }; export default DataComponent;
To see the React service pattern in action, look through numerous open-source projects on GitHub.
Here are some instances:
- React Boilerplate: This boilerplate is widely used and well-maintained for developing scalable React apps. It makes considerable use of the service design to manage side effects and business logic. React Boilerplate GitHub repository.
- Create React App Example: The official create-react-app repository provides examples of how to structure a React application using best practices, such as the service pattern. Create a React App GitHub Repository.
- React Redux RealWorld Example: This project is a complete example of a real-world application created using React and Redux. It uses best practices, such as the service pattern, to manage state and business logic.
React Service Pattern Tutorial: Step-by-Step Guide
Using the power of React and the simplicity of the Facade Pattern to improve web service design.
With the rise of modern online applications, maintaining modularity, adaptability, and maintainability has become increasingly important.
One method to accomplish this is to use design patterns efficiently. Today, we will use the Facade Pattern to create a versatile HttpService for React apps.
What is the Facade Pattern?
The Facade Pattern creates a simpler interface for a more complicated component. Instead of sending HTTP requests with various headers and configurations throughout your program, we’ll establish a centralized service (HttpService) to hide the complexity. This:
- Reduces errors by centralizing logic.
- Improves code readability and maintainability.
- Provides the ability to alter the underlying library (such as axios) without affecting the rest of the application.
Step 1: Set Up
Before we begin, make sure you have Axios installed: npm install axios.
Step 2: Basic Structure.
Let’s begin by creating a basic structure for our service. This manages our default base URL and instantiates Axios:
import axios from 'axios'; class HttpService { constructor(baseURL = 'https://api.yourdomain.com') { this.baseUrl = baseURL; this.instance = axios.create({ baseURL: this.baseUrl }); } }
Step 3: Handling Headers
To handle authentication tokens and other headers effectively:
get defaultHeaders() { return { 'Authorization': localStorage.getItem('Authorization'), 'Content-Type': 'application/json', }; }
Step 4: Rather than repeating logic for get, post, etc., there will be a core request method:
Async request (method, url, data=null, customHeaders={}) { const headers = { ...this.defaultHeaders, ...customHeaders }; const source = axios.CancelToken.source(); const config = { method, url, headers, cancelToken: source.token }; if (data) { config.data = data; } return { request: this.instance(config), cancel: source.cancel }; }
Step 5: We’ll use the core request method to define specific HTTP methods:
get(url, customHeaders = {}) { return this.request('get', url, null, customHeaders); } post(url, data, customHeaders = {}) { return this.request('post', url, data, customHeaders); } put(url, data, customHeaders = {}) { return this.request('put', url, data, customHeaders); } delete(url, customHeaders = {}) { return this.request('delete', url, null, customHeaders); }
React services example using JSONPlaceholder API
Let’s begin by building a simple data service that retrieves data from an API. For this React service example, we’ll use the JSONPlaceholder API, which provides a collection of fictitious online REST APIs for testing and prototyping.
First, make a new directory called services in your src folder. Create a DataService.js file in this directory:
javascript
// src/services/DataService.js const API_URL = 'https://jsonplaceholder.typicode.com'; const getData = async (endpoint) => { try { const response = await fetch(`${API_URL}/${endpoint}`); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); return data; } catch (error) { console.error('Failed to fetch data:', error); throw error; } }; export default { getData };
In this file, we define the getData method, which accepts an endpoint parameter, generates the whole API URL, retrieves data from the API, and returns it as a JSON object. If the fetch operation fails, an error is returned.
Using the Data Service in a React Component
Now that we’ve configured our data service, let’s use it in a React component. We’ll build a basic component that retrieves and shows a list of posts from the JSONPlaceholder API.
Create a new component
Make a new file called PostList.js in your src/components directory.
javascript
// src/components/PostList.js import React, { useEffect, useState } from 'react'; import DataService from '../services/DataService'; const PostList = () => { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchPosts = async () => { try { const data = await DataService.getData('posts'); setPosts(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchPosts(); }, []); if (loading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error}</div>; } return ( <div> <h1>Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }; export default PostList;
In this component, we use the useState hook to manage the posts, loading, and error state variables. The useEffect hook is used to retrieve data when the component is mounted. The fetchPosts function runs our DataService’s getData method, updates the state with the obtained posts, and handles any errors encountered during the get operation.
What is the Advanced Use of React Services?
1. Managing State with Context and Hooks
As your application expands, you may find it useful to manage state with React’s Context API and custom hooks in conjunction with services. This approach enables a more scalable and maintainable state management solution.
Creating a Context and Provider: Make a new file called PostContext.js in your src/context directory.
javascript
// src/context/PostContext.js import React, { createContext, useState, useEffect } from 'react'; import DataService from '../services/DataService'; const PostContext = createContext(); const PostProvider = ({ children }) => { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchPosts = async () => { try { const data = await DataService.getData('posts'); setPosts(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchPosts(); }, []); return ( <PostContext.Provider value={{ posts, loading, error }}> {children} </PostContext.Provider> ); }; export { PostContext, PostProvider };
We use the createContext function to build a PostContext and a PostProvider component, which fetches the posts and passes the posts, loading, and error status values to its children using the PostContext.Provider method.
Consuming Context in a Component: Update the PostList component to use the context.
javascript
// src/components/PostList.js import React, useContext from 'react'; import { PostContext } from '../context/PostContext'; const PostList = () => { const { posts, loading, error } = useContext(PostContext); if (loading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error}</div>; } return ( <div> <h1>Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }; export default PostList;
In this revised PostList component, we use the useContext hook to consume the PostContext and access the posts, loading, and error states.
React Design Patterns and Best Practices
If you’re creating a React application and want to keep it tidy and scalable, you should follow certain best practices! In this section, we’ll go over four fundamental best practices for creating React applications that are maintainable, scalable, and efficient.
These practices include arranging your project files in a feature-based folder structure, keeping your components focused and minimal, following proper naming conventions, and separating the duties of pages and presentational components.
By following these guidelines, you can develop applications that are easier to access, understand, and manage.
1. Folder Structure
A well-organized folder structure is essential for keeping your project hierarchy clear and easy to explore. Look at this example of a feature-based folder organization.
Rather than arranging components by file type, we categorize them by feature. This makes it much easier to locate and handle associated files, such as a component’s JavaScript and CSS files.
2. Keep the components small and focused
When developing an application, it is critical to design components that are simple to comprehend, maintain, and test.
That’s why it’s best to keep your components concentrated and tiny. If a component is becoming too large, don’t worry; simply divide it down into smaller, more manageable components!
For example, suppose you have a UserProfile component that is beginning to seem overwhelming. You might separate it into smaller components such as ProfilePicture, UserName, and UserBio. This will make each component easier to manage and reuse.
3. Naming Conventions
Giving meaningful names to your components, props, and state variables makes it easier for others (and future you!) to comprehend your code. Furthermore, it makes your code easier to maintain in the long term. Here are some pointers to help you name things like a pro.
- For components, use PascalCase (like UserProfile.js)
- For variables and functions, use camelCase (like getUserData())
- And for constants, use UPPERCASE_SNAKE_CASE (like API_URL)
Keep in mind that this is highly subjective, and it is entirely up to you to select which naming standard to use. The most important thing is to stay consistent with your naming technique.
The Two Types of Components: Pages (container) and Presentational Components
React has two sorts of components: pages (container components) and presentational components. Pages perform functions such as retrieving data from other sources (such as APIs), managing state and logic, and passing data down to presentational components via props.
Meanwhile, presentational components are in charge of rendering UI elements and displaying data passed down by their parent components. Separating these roles allows us to design more modular and reusable components.
Okay, so we can categorize React components into two types: pages (or container components) and presentational components. Pages handle things like collecting data from APIs and managing state and logic, whereas presentational components display the data they receive from their props and define how things seem. This allows us to keep our code more organized, understandable, and testable.
Conclusion
In this article, we learned about some practical React Service patterns, examples, and best practices for 2024.
Service patterns are useful because they allow us to draw on the expertise of all the developers who produced and vetted these patterns. As a result, they can save development time while also boosting software quality.