As modern web applications grow in complexity, traditional monolithic frontend architectures often become bottlenecks for development teams. Microfrontends offer a solution by breaking down large applications into smaller, independent pieces that can be developed and deployed separately. In this article, we'll explore how single-spa parcels with React components provide a powerful way to implement microfrontends.
While microfrontends offer powerful architectural benefits, they're not always the right choice for every project. Understanding when to adopt this pattern is crucial for making the right technical decisions for your team.
For Smaller Teams: When you have a smaller team working on a frontend application, a traditional monolithic architecture often works perfectly fine. The overhead of setting up and maintaining a microfrontend architecture might outweigh its benefits. A single codebase is simpler to manage, easier to refactor, and requires less infrastructure complexity.
For Larger Teams: As your organization grows and multiple teams need to contribute to the same frontend application, things become more challenging. A single frontend monolith can quickly become a bottleneck where:
This is where microfrontends shine. It makes sense to split different parts of your web application into independent applications, each with its own:
The microfrontend architecture uses a shell application (or container) that orchestrates these independent applications and puts them together into a cohesive user experience. This shell handles routing, communication between microfrontends, and shared resources, while each team maintains full autonomy over their part of the application.
The key benefit? Teams can deploy and release independently, dramatically increasing development velocity and reducing coordination overhead. A bug fix in one microfrontend doesn't require redeploying the entire application, and new features can be rolled out progressively without affecting other teams.
Now that we understand when microfrontends make sense, let's explore how to implement them effectively. Single-spa is one of the most popular frameworks for building microfrontend architectures, and parcels are its core building block for creating reusable, framework-agnostic components that can be shared across your application ecosystem.
Parcels are single-spa's way of creating framework-agnostic components that can be mounted anywhere in your application. They are self-contained units with their own lifecycle that can be shared across different parts of your application. When combined with React functional components, parcels become even more powerful and easier to manage.
The power of parcels becomes evident when building complex applications with multiple teams. For example, imagine you're building an e-commerce platform where:
All three parcels can coexist in the same application shell, communicate through a shared event bus, and be deployed independently. The shopping cart can be updated without touching the recommendations system, and authentication changes don't require rebuilding the entire application.
Throughout this article, we'll build practical examples using React functional components, demonstrating how to create parcels, wrap them for easy consumption, and integrate them into a cohesive application. By the end, you'll have a complete understanding of how to architect and implement a production-ready microfrontend system.
Learn more about single-spa at https://single-spa.js.org/
A parcel in single-spa is a framework-agnostic component with three lifecycle methods: mount, unmount, and optionally update. Think of it as a self-contained micro-application that can be dynamically loaded and rendered anywhere in your application.
Understanding the Lifecycle Methods:
mount(props): Called when the parcel needs to be rendered. Receives props including domElement (where to render) and any custom props. Returns a Promise that resolves when mounting is complete.
unmount(props): Called when the parcel needs to be removed from the DOM. Responsible for cleanup, including removing event listeners and unmounting React components. Returns a Promise.
update(props): (Optional) Called when props change while the parcel is already mounted. Allows for efficient re-rendering without full unmount/remount cycles. Returns a Promise.
In this example, we'll create a user profile parcel. The parcel configuration defines how the component should be mounted (rendered to the DOM), unmounted (cleaned up), and updated (when props change). This gives you complete control over the component's lifecycle while maintaining framework independence.
Note how we store the React root reference on the domElement itself. This pattern ensures we can properly unmount the React application later and update it when props change. The key advantage is that this parcel can be used in any part of your application - or even in different applications entirely - as long as they're running within the single-spa ecosystem.
1import React from "react"2import ReactDOM from "react-dom/client"34const UserProfile = ({ user, onUserUpdate }) => (5 <div className="user-profile">6 <img src={user.avatar} alt={user.name} />7 <h3>{user.name}</h3>8 <p>{user.email}</p>9 <button onClick={() => onUserUpdate({ ...user, status: "updated" })}>10 Update Profile11 </button>12 </div>13)1415const userProfileParcel = {16 mount: (props) => {17 return Promise.resolve().then(() => {18 const { domElement, ...parcelProps } = props19 const root = ReactDOM.createRoot(domElement)20 domElement.__root = root // store for unmount21 root.render(<UserProfile {...parcelProps} />)22 })23 },24 unmount: (props) => {25 return Promise.resolve().then(() => {26 props.domElement.__root?.unmount()27 delete props.domElement.__root28 })29 },30 update: (props) => {31 return Promise.resolve().then(() => {32 const { domElement, ...parcelProps } = props33 domElement.__root?.render(<UserProfile {...parcelProps} />)34 })35 },36}3738export default userProfileParcel
While parcels are framework-agnostic, we need a React-friendly way to work with them. This wrapper component bridges the gap between React's component lifecycle and single-spa's parcel lifecycle, making parcels feel like native React components.
Why Do We Need a Wrapper?
Without this wrapper, you'd need to manually manage parcel mounting and unmounting in each component where you use a parcel. The wrapper abstracts away this complexity and provides:
The ReactParcel component uses React hooks to manage the parcel's lifecycle. It leverages useRef to maintain references to the parcel instance and DOM container, ensuring they persist across renders. The first useEffect handles mounting and unmounting, while the second handles prop updates when the parcel's data changes.
This wrapper makes using parcels as simple as rendering any other React component, while still maintaining the full power and flexibility of single-spa parcels. You can reuse this wrapper throughout your application for any parcel you create, regardless of which framework the parcel itself uses.
1import React, { useEffect, useRef } from "react"2import { mountRootParcel } from "single-spa"34const ReactParcel = ({ config, props }) => {5 const parcelRef = useRef(null)6 const containerRef = useRef(null)78 useEffect(() => {9 if (containerRef.current) {10 parcelRef.current = mountRootParcel(config, {11 domElement: containerRef.current,12 ...props,13 })14 }1516 return () => {17 if (parcelRef.current) {18 parcelRef.current.unmount()19 }20 }21 }, [config])2223 useEffect(() => {24 if (parcelRef.current && parcelRef.current.update) {25 parcelRef.current.update(props)26 }27 }, [props])2829 return <div ref={containerRef} />30}3132export default ReactParcel
With both the parcel configuration and wrapper component in place, integrating parcels into your application becomes straightforward. This is where the benefits of the microfrontend architecture really shine - you can drop parcels anywhere in your component tree.
In this example, we're building a typical application layout with multiple parcels: a user profile in the sidebar and a notification center in the main content area. Each parcel is loaded independently, demonstrating how different teams can build and maintain separate features that work together seamlessly. Notice how we pass data and callback functions through the props object - parcels behave just like regular React components from the parent's perspective.
The beauty of this approach is that the userProfileParcel and notificationParcel could be maintained by different teams, deployed independently, and even shared across multiple applications, all while integrating seamlessly into your React application.
1import React, { useState } from "react"2import ReactParcel from "./ReactParcel"3import userProfileParcel from "./parcels/userProfileParcel"4import notificationParcel from "./parcels/notificationParcel"56const MainApplication = () => {7 const [user, setUser] = useState({8 name: "Sarah Chen",9 email: "sarah.chen@company.com",10 avatar: "/avatar.jpg",11 })1213 const [notifications] = useState([14 { id: 1, message: "New comment on your pull request", type: "info" },15 {16 id: 2,17 message: "Build successful for your project dashboard",18 type: "success",19 },20 ])2122 return (23 <div className="app">24 <header className="app-header">25 <h1>Microfrontend Application</h1>26 </header>2728 <div className="app-layout">29 <aside className="sidebar">30 <ReactParcel31 config={userProfileParcel}32 props={{33 user,34 onUserUpdate: setUser,35 }}36 />37 </aside>3839 <main className="content">40 <ReactParcel41 config={notificationParcel}42 props={{43 notifications,44 userId: user.name,45 }}46 />47 </main>48 </div>49 </div>50 )51}5253export default MainApplication
With these three components in place - the parcel configuration, the React wrapper, and the application integration - you have a complete microfrontend setup. As demonstrated in the example above, multiple parcels can coexist in the same application, each independently developed and maintained. The user profile and notification center parcels could be built by different teams, using different release cycles, yet they integrate seamlessly into the main application.
The pattern is repeatable for as many parcels as you need, allowing you to scale your application architecture horizontally by adding new independent features without increasing complexity. This approach enables teams to work autonomously on different features, deploy them separately, and compose them together into a cohesive user experience. The result is a more maintainable, scalable architecture that grows naturally with your organization.
While microfrontends with parcels offer tremendous benefits, there are important considerations to keep in mind:
Performance Optimization
Communication Between Parcels Parcels often need to communicate with each other. Common patterns include:
Error Handling Robust error handling is critical in a microfrontend architecture:
Microfrontends with single-spa parcels offer a powerful solution for scaling frontend architectures in growing organizations. By leveraging parcels with React functional components, you can build sophisticated applications that balance team autonomy with a unified user experience.
Key Takeaways:
Ready to begin? Extract one focused piece of functionality into a parcel today and experience firsthand how microfrontends can transform your development workflow and team collaboration!