Micro frontends: Cross-application communication with Single-Spa and RxJS.
In the previous article “Connect Micro frontends with the Single-Spa framework. Step by step guide.”, we were connecting multiple frameworks with the Single-Spa (such as Angular, React, Vue, and Svelte). In this one, we are going to organize micro frontend interaction (component communication) with the RxJs. We also will use a different codeshare strategy such as Git Submodules to keep a single source of truth and easily share the core base across the apps. As previously, we will try to keep it simple as much as we can to focus more on the base concepts and practice.
As a project idea, we’ll build a Todo app. Where the users can enter text and by pressing add button include a todo item to the list. Users will also be able to remove added items using the close button.
Overall, we are going to have four projects:
- Todo-Core (TypeScript)
- Todo (Single-Spa)
- Todo-Form (Angular)
- Todo-List (React)
Todo-Core (Git Submodules)
The core module will be used across all apps and it’s going to have all core logic with the common models and services. Further, it will be used as a Git submodule.
Note: Instead of Git Submodules you can use the node packages as a codeshare strategy as well, and it even might be easier.
- Create a repository with the name
todo-core
(can be with theREADME.md
) - Clone your repository:
git clone http://github.com/your_username/todo-core.git
3. Navigate to project:
cd todo-core
4. Init Node project:
npm init --yes
5. Install rxjs
library:
npm install rxjs
6. Init TypeScript project:
tsc --init
7. Update the tsconfig.json:
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
8. Add .gitignore:
node_modules
9. Add src/base.service.ts:
import { BehaviorSubject } from "rxjs";export abstract class BaseService {
abstract list$: BehaviorSubject<string[]>;
abstract addTodo(todo: string): void;
abstract removeTodo(index: number): void;
}
10. Add src/base.service.impl.ts:
import { BehaviorSubject } from "rxjs";import { BaseService } from "./base.service";export const BaseServiceImpl: BaseService = Object.freeze({
list$: new BehaviorSubject<string[]>([]),
addTodo(todo: string): void {
this.list$.next([...this.list$.getValue(), todo]);
},
removeTodo(todoIndex: number): void {
const updatedList = this.list$
.getValue()
.filter((el, index) => index !== todoIndex);this.list$.next(updatedList);
},
});
11. Add src/index.ts:
export * from "./base.service";
export * from "./base.service.impl";
12. Commit and push changes to remote:
git add .
git commit -m "Core project setup"
git push
Todo (Single-Spa layer app)
This project will be responsible for the framework composition and content layout.
- Create a project folder and navigate there:
mkdir todo && cd todo
2. Init Single-Spa layout project:
create-single-spa --layout
and choose:
- Directory for new project —
.
- Select type to generate —
single-spa root config
- Which package manager do you want to use? —
npm
- Will this project use TypeScript —
y
- Organization name —
obaranovskyi
3. Install dependencies:
npm install
4. Open src/microfrontend-layout.html, and remove the following line:
<application name="@single-spa/welcome"></application>
5. Open src/index.ejs, and remove the line with:
"@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js",
Submodule Integration:
6. Add todo-core
as a Git submodule:
git submodule add https://github.com/your_user/todo-core.git
7. Add src/externals.ts:
export * from "../todo-core/src/index";
8. Update src/obaranovskyi-root-config.ts:
import { BaseServiceImpl } from "./externals";// ...window["todoCore"] = BaseServiceImpl;
9. Start the project:
npm start
Todo-Form app (Angular)
Within this project, we are going to implement todo form with the submit button.
- Create Angular app using Angular CLI
ng new todo-form
and choose:
- Do you want to enforce stricter type checking and stricter bundle budgets in the workspace? —
y
- Would you like to add angular routing? —
n
- Which stylesheet format would you like to use? —
SCSS
2. Navigate the project and install dependencies:
cd todo-form && npm install
3. Setup Single-Spa:
ng add single-spa-angular
and choose:
- Does your application use angular routing? —
n
- Does your application use the browserAnimationModule —
n
Submodule Integration:
4. Add todo-core
as a Git submodule:
git submodule add https://github.com/your_user/todo-core.git
5. Add paths
with todo-core
mapping to the tsconfig.json:
{
"compilerOptions": {
...
"baseUrl": "./",
"paths": {
"@todo-core/*": ["./todo-core/src/*"]
}
},
...
}
Note: the baseUrl
has to be in place as well.
6. Update the src/app/app.module.ts:
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';import { BaseService } from '@todo-core/base.service';
import { AppComponent } from './app.component';
import { TodoFormComponent } from './todo-form/todo-form.component';@NgModule({
declarations: [AppComponent, TodoFormComponent],
imports: [BrowserModule, FormsModule],
providers: [{ provide: BaseService, useValue: (window as any).todoCore }],
bootstrap: [AppComponent],
})
export class AppModule {}
7. Update the src/app/app.component.html:
<app-todo-form></app-todo-form>
Todo form component implementation
8. Generate todo-form
component:
ng generate component todo-form
9. Update src/app/todo-form/todo-form.component.html:
<div class="todo-form">
<div class="todo-form-container">
<input type="text" [(ngModel)]="todo" placeholder="Enter text ..." class="todo-form-input">
<button (click)="addTodo()" class="todo-form-submit">Add</button>
</div>
</div>
10. Update src/app/todo-form/todo-form.component.ts:
import { Component } from '@angular/core';import { BaseService } from '@todo-core/base.service';@Component({
selector: 'app-todo-form',
templateUrl: './todo-form.component.html',
styleUrls: ['./todo-form.component.scss'],
})
export class TodoFormComponent {
todo!: string;constructor(private readonly baseService: BaseService) {}addTodo(): void {
if (!this.todo) {
return;
}this.baseService.addTodo(this.todo);
this.todo = '';
}
}
11. Update src/app/todo-form/todo-form.component.scss:
.todo-form {
position: relative;
left: calc(50% - 250px);
top: 200px; &-input {
width: 500px;
padding: 11px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
} &-submit {
background-color: crimson;
border: none;
color: white;
padding: 10px 40px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin-left: 15px;
}
}
12. Install dependencies:
npm install
13. Run the project:
npm run serve:single-spa:todo-form
Todo project updates:
14. Include zone.js library src/index.ejs:
<script src="https://unpkg.com/zone.js"></script>
15. Update SystemJS import map in the src/index.ejs:
<script type="systemjs-importmap">
{
"imports": {
"@obaranovskyi/root-config": "//localhost:9000/obaranovskyi-root-config.js",
"@obaranovskyi/todo-form": "//localhost:4200/main.js"
}
}
</script>
16. Register app in the src/obaranovskyi-root-config.ts:
registerApplication(
"@obaranovskyi/todo-form",
() => System.import("@obaranovskyi/todo-form"),
(location) => true
);
Todo List (React)
Within this project, we will implement the todo list and remove functionality.
- Create folder project and change directory:
mkdir todo-list && cd todo-list
2. Setup Single-Spa/React project:
create-single-spa --framework react
and choose:
- Directory for new project —
.
- Which package manager do you want to use? —
npm
- Will this project use TypeScript —
y
- Organization name —
obaranovskyi
- Project name —
todo-list
3. Install dependencies:
npm install
4. Install rxjs
library:
npm install rxjs
Submodule Integration:
5. Add todo-core
as a Git submodule:
git submodule add https://github.com/your_user/todo-core.git
6. Add src/externals.ts:
import { BaseService } from "../todo-core/src/base.service";export const baseService: BaseService = (window as any).todoCore as BaseService;
export * from "../todo-core/src/index";
7. Add src/TodoList.tsx:
import React from "react";
import { Subject, takeUntil } from "rxjs";
import { baseService } from "./externals";
import "./TodoList.css";export interface IProps {}
export interface IState {
todos: string[];
}export class TodoList extends React.Component<IProps, IState> {
destroy$: Subject<void> = new Subject<void>();constructor(props) {
super(props);
this.state = { todos: [] };
}componentDidMount(): void {
this.observeTodos();
}observeTodos(): void {
baseService.list$
.pipe(takeUntil(this.destroy$))
.subscribe((list: string[]) => {
this.setState({ todos: list });
});
}componentWillUnmount(): void {
this.destroy$.next();
this.destroy$.complete();
}removeTodo = (index: number) => {
baseService.removeTodo(index);
};render() {
return (
<div className="main">
<div className="container">
<div className="total">Total: {this.state.todos.length}</div>
<div className="list">
<ol>
{this.state.todos.map((todo: string, index: number) => (
<li key={index}>
{todo}{" "}
<span
role="button"
tabIndex={0}
className="close"
onClick={() => this.removeTodo(index)}
>
[x]
</span>
</li>
))}
</ol>
</div>
</div>
</div>
);
}
}
8. Add src/TodoList.css:
.main {
font-family: sans-serif;
position: relative;
top: 200px;
left: calc(50% - 250px);
}.container {
max-width: 1000px;
}.total {
text-decoration: underline;
}
.close {
cursor: pointer;
color: red;
}
9. Update root.component.tsx:
import { TodoList } from "./TodoList";export default function Root(props) {
return <TodoList />;
}
10. Run the project:
npm start -- --port 8500
Todo project updates:
11. Update src/index.ejs:
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js",
"@obaranovskyi/todo-list": "//localhost:8500/obaranovskyi-todo-list.js",
"@obaranovskyi/root-config": "//localhost:9000/obaranovskyi-root-config.js",
"@obaranovskyi/todo-form": "//localhost:4200/main.js"
}
}
</script>
12. Register app in the src/obaranovskyi-root-config.ts:
registerApplication(
"@obaranovskyi/todo-list",
() => System.import("@obaranovskyi/todo-list"),
(location) => true
);
Now everything should be working fine! :)
Conclusion
Thank you guys for reading. I hope you enjoyed it and learned some new stuff related to JavaScript. Please subscribe and press the ‘Clap’ button if you like this article.