Skip to main content

· 10 min read

Motivation

As modern front-end applications become larger, making full use of the device's CPU multi-cores to improve performance may become an important trend.

Front-end applications often run in a single browser window, and JavaScript runs on a single thread. This means that common web applications cannot take full advantage of a CPU's multiple-cores. As applications become larger and more complex, this can lead to performance problems and a poor user experience.

However, there is good news (the gradual phasing out of IE and Safari v16 support for Shared Worker). Modern browsers widely support various types of Workers, including Shared Workers. Shared Workers are a mature technology that allows multiple threads of JavaScript code to share data and communicate with each other. This makes them ideal for building multi-process front-end applications.

Multi-process front-end applications have several benefits. They can better resolve computation-intensive and slow-running JavaScript, which can improve performance and fluidity. They can also increase the number of concurrent requests that can be processed, which can improve the responsiveness of the application.

So we aim to explore a Web application framework that leverages multi-processing.

Web application with Multi-Processing

In a multi-process web architecture, we can leverage the Shared Web Apps concept of reactant-share to extend general multi-process programming.

Shared Web Apps allows running web applications in multiple browser windows or workers. It uses a unique front-end server (like a Shared Worker) to share web apps, whether it's code sharing, local storage sharing, state sharing, and so on. Regardless of how many browser windows are opened, there's always only one server application instance shared among multiple client applications for the Shared Web Apps. It enables Web Tabs to only perform rendering separation, thus making better use of the device's multi-cores and ensuring smooth operation of the web application.

Shared Web Apps provides the following benefits:

  • Reduces the mental burden of multi-process programming by implementing Isomorphism with a universal modular model. Isomorphism is the ability to execute the same code on both the server process, client process or other process, which simplifies multi-process programming.
  • Ensures smooth operation of the front-end server process by transferring compute-intensive tasks to another process. This frees up the front-end server process to focus on business logic and the client process to focus on rendering, which improves performance and responsiveness.
  • Improves request concurrency by using a better multi-process model. This allows the web application to handle more requests simultaneously.

Coworker based on reactant-share

Based on reactant-share, we have implemented the Coworker model, which facilitates state sharing across multiple processes, synchronizes state, and minimizes state changes with patches to ensure optimal performance in multi-process execution.

Workflow

The Coworker model consists of three types of processes:

  • Client Process: The rendering process, which accepts shared state and only renders the web UI. It is lightweight to ensure smooth rendering.
  • Server Process: The main process, which executes most of the application business logic. It should also ensure smooth running.
  • Coworker Process: The process responsible for compute-intensive business or request-intensive logic. This process frees up the server process to focus on business logic. The server process can reduce blocking caused by JavaScript and is less susceptible to the effects of request-intensive logic.

In "Base" mode, Reactant Shared Apps has only two processes: the Tab process and the Coworker process. The Coworker process uses a Web Worker by default.

Implementation of Coworker

For the related principles of Reactant-Share, please see the following link: https://reactant.js.org/blog/2021/10/03/how-to-make-web-application-support-multiple-browser-windows

Coworker consists of two modules:

  • CoworkerAdapter: Provides transport for communication between the server process and the coworker process.
  • CoworkerExecutor: Handles synchronization of shared state between processes and custom Coworker type modules (used for proxy execution of coworkers). Coworkers are synchronously sent to the main process in one direction. Each time a Coworker syncs its state, it carries a sequence tag. If the sequence is abnormal, a complete Coworker state synchronization is triggered automatically to ensure the consistency of the shared state between the Coworker and the main process.

Core Concepts and Advantages of Coworker

  • Isomorphism: All processes execute the same code, which enhances the maintainability of multi-process programming in JavaScript.
  • Process Interaction based on the Actor Model: Relying on the Actor model, this method reduces the cognitive load of multi-process programming in JavaScript.
  • Generic Transport Model: Coworker supports any transport based on data-transport (https://github.com/unadlib/data-transport), so it can run in any container that supports transport, including SharedWorker. The following is a list of supported transports:
    • iframe
    • Broadcast
    • Web Worker
    • Service Worker
    • Shared Worker
    • Browser Extension
    • Node.js
    • WebRTC
    • Electron
    • Any other port based on data-transport
  • High Performance Based on Mutative: Mutative is faster than the naive handcrafted reducer and 10x faster than Immer. Updates to immutable data based on Mutative also maintain good performance. The patches obtained from the shared state update are used for state synchronization.
  • High Performance: Due to Coworker taking on a large number of requests and compute-intensive tasks, the main process and rendering process maintain extremely high performance and user experience.
  • Support for Large Applications: Reactant provides a complete module model design, including dependency injection and class first, as well as various modular design and dynamic module injections.
  • Separation of Service and Rendering View Modules: Service modules, which are primarily based on business logic, can execute separately from view modules. This not only achieves separation of concerns but also allows the process to have its own containerization.
  • Graceful Degradation: If the JavaScript host environment does not support SharedWorker, Coworker reverts to a regular SPA. This does not affect the behavior of any current application.

API

delegate() - It will forward execution to the module and specified function proxies in Coworker, inspired by the Actor model.

Examples

We will create a Counter application with Coworker based on the ‘Base‘ pattern.

  1. Firstly, create app.tsx that contains the ProxyCounter module which needs to be executed in Coworker.

Its calling method delegate(this.proxyCounter, 'increase', []) is exactly the same as that of general Shared Web Apps. Whether it will be executed with a proxy in Coworker depends on the configuration of createApp.

import React from "react";
import {
ViewModule,
injectable,
useConnector,
action,
state,
delegate,
} from "reactant-share";

@injectable({
name: "ProxyCounter",
})
export class ProxyCounter {
@state
count = 0;

@action
increase() {
this.count += 1;
}
}

@injectable({
name: "AppView",
})
export class AppView extends ViewModule {
constructor(public proxyCounter: ProxyCounter) {
super();
}

@state
count = 0;

@action
increase() {
this.count += 1;
}

component(this: AppView) {
const [count, proxyCount] = useConnector(() => [
this.count,

this.proxyCounter.count,
]);

return (
<>
<div>{count}</div>
<button type="button" onClick={() => delegate(this, "increase", [])}>
+
</button>
<p>proxy in coworker</p>
<div>{proxyCount}</div>
<button
type="button"
onClick={() => delegate(this.proxyCounter, "increase", [])}
>
+
</button>
</>
);
}
}
  1. Create the main file index.ts. Here, we set ProxyCounter as a module of Coworker, and set isCoworker to false.
import { render } from 'reactant-web';
import {
createSharedApp,
Coworker,
CoworkerOptions,
ICoworkerOptions,
} from 'reactant-share';
import { AppView, ProxyCounter } from './app';

createSharedApp({
modules: [
Coworker,
{
provide: CoworkerOptions,
useValue: {
useModules: [ProxyCounter],
worker: new Worker(new URL('./coworker.ts', import.meta.url)),
isCoworker: false,
} as ICoworkerOptions,
},
],
main: AppView,
render,
share: {
name: 'SharedWorkerApp',
type: 'Base',
},
}).then((app) => {
app.bootstrap(document.getElementById('app'));
window.app = app;
});
  1. Create the Coworker file coworker.ts. Here, we also set ProxyCounter as a module of Coworker, but set isCoworker to true.
import {
createSharedApp,
Coworker,
CoworkerOptions,
ICoworkerOptions,
} from 'reactant-share';
import { AppView, ProxyCounter } from './app';

createSharedApp({
modules: [
Coworker,
{
provide: CoworkerOptions,
useValue: {
useModules: [ProxyCounter],
isCoworker: true,
} as ICoworkerOptions,
},
],
main: AppView,
render: () => {},
share: {
name: 'SharedWorkerApp',
type: 'Base',
},
}).then((app) => {
self.app = app;
});

So far, we have completed a basic application with a Coworker. Users trigger the delegate(this.proxyCounter, 'increase', []) in the main process via the UI. It will be forwarded to the coworker to execute the increase function of proxyCounter, and the shared state will automatically synchronize back to the main process. The rendering update is completed by the useConnector() Hook.

Q&A

1. What are the challenges of multi-process programming with Coworker based on reactant-share?

State sharing and synchronization among processes in multi-process programming are relatively complex. Fortunately, Reactant-share ensures robustness through a shared state design with consistency. The dependencies between isomorphic modules of Coworker should also be taken into account. In development, concepts such as Domain-Driven Design should be practiced as much as possible to avoid incorrect module design.

2. What are the possible use case types for Coworker?

  • Request Queue - Coworker is particularly suitable for modules with intensive requests. Running these in Coworker ensures they don't occupy the main process's request queue, allowing other main process requests to execute.
  • Large Task Execution Blocking - When a computationally intensive task is executed, the application's main process should not be blocked. Such tasks are well suited for asynchronous execution in Coworker.
  • Isolatable Modules - Coworker can also be used as a sandbox to isolate execution of some modules.

3. Are there any specific examples to demonstrate that Coworkers can improve application performance?

In production, we've introduced Coworker into some specific scenarios for modules related to large data volume text matching. It resulted in a substantial performance improvement, even up to 10x more, significantly enhancing the user experience.

Such computationally intensive text matching used to require users to wait more than 1s in the past, with the webpage being completely blocked. However, after using Coworker, the webpage blockage was reduced to less than 100ms (of course, the actual degree of improvement varies with different data sizes).

4. Is Coworker usable across different browsers, or does it only support within browser tabs? Can Coworker be used across tabs in different domains?

Coworker is a multi-process model based on reactant-share, and reactant-share is based on data-transport. Therefore, we only need to use WebRTC transport from data-transport in CoworkerAdapter within Coworker to achieve cross-browser support. Additionally, to support usage across tabs in different domains, we can implement the use of Coworker under cross-domain tabs with an approach using iframe + shared worker.

Conclusion

Front-end development is at a turning point, driven by advances in front-end technology and browser capabilities. Multi-core CPUs and multi-process tools such as Shared Workers and other Workers are now being used to great effect in front-end development. The emergence of Shared Web Apps with Coworker introduces a new multi-process model for front-end applications, which significantly improves application performance, user experience, and code maintainability. For developers, this means more technical choices and challenges, but also more opportunities and potential.

Multi-process programming for front-end applications is likely to become a key solution for improving front-end performance. This would result in a smoother, more efficient, and more responsive user experience.

· 14 min read

Motivation

When we develop a Single-Page Application, we usually only define its behavior in a single browser window, and even if the same application is opened on multiple browser windows, in most cases it is only synchronized with the local storage, and the state of each application in each window is not synchronized in real time (unless the server synchronizes), they run in isolation and are relatively independent.

However, this means that more browser windows will generate more and more independent application instances, which may have different UI states and often inevitably have the same network requests or WebSocket connections, which may also mean a bad user experience (as users may have become accustomed to) and excessive usage of server resources.

So what does it mean to have applications that supports multiple browser windows?

  • Application instance sharing: code sharing, local storage sharing, state sharing, and more
  • Lower server resource usage
  • Better user consistency experience
  • Smoother web applications

But it's not easy to keep large Web applications running smoothly.

Web applications are still primarily built in JavaScript, which is a single-threaded programming language, and slow JavaScript code can prevent the browser’s rendering. The good news is that mainstream browsers are gradually supporting more different types of workers, especially Service Workers, which are used to implement PWAs (Progressive Web Apps) that greatly enhance the user experience. And the latest modern browsers also provide Web Worker, Shared Worker. With IE becoming deprecated this year, there is good support for these workers.

So what does it mean for Web applications to be "multi-threaded" with Worker?

"The State Of Web Workers In 2021" post covers a number of unpredictable performance issues. With these browser workers we will likely be better able to deal with computationally complex and slow-running JS code to keep web applications smooth.

It's time to rethink why we can't make web applications support multiple browser windows and improve the performance of web applications. New architectural requirements bring new framework requirements, and such applications we call it Shared Web Apps.

Shared Web Apps

Even though we want users to open as few application windows as possible, the fact remains that many users will open the same application in multiple browser windows.

Shared Web Apps supports running web applications in multiple browser windows.

It has a unique Server thread to share the Shared Web Apps, whether it's code sharing, local storage sharing, state sharing, and so on. No matter how many browser windows are opened, Shared Web Apps always has only one server app instance for multiple client apps sharing. We all know that DOM operations are expensive. In Shared Web Apps, the client app instance is only responsible for rendering, and except for state sync the client app will become very lightweight and almost all business logic will run in the server app.

  • The client app only renders UI, making better use of the device's multiple cores to ensure that the client app is smooth
  • Solve the problems caused by multiple browser windows
  • Better separation of concerns

reactant-share - A framework for building Shared Web Apps

To build such Shared Web Apps, reactant-share was created. reactant-share is based on the reactant framework and react library, which supports the following features.

  • Dependency injection
  • Immutable state management
  • View module
  • Redux plug-in module
  • Test bed for unit testing and integration testing
  • Routing module
  • Persistence module
  • Module dynamics
  • Shared web app support multiple browser windows
    • Shared tab
    • SharedWorker
    • Detached window
    • iframe

reactant-share is very easy to use, you can use it to quickly build a Shared Web Apps. it greatly reduces the complexity of supporting multi-browser window application architecture.

How it works

When reactant-share starts, it creates a server app instance and multiple client app instances (one per browser window) in the browser, but the only instance that is really running in full is the server app instance, which is responsible for almost all of the application's logic, and multiple client app instances simply synchronize state and render. The state model of reactant-share uses immutable state, and reactant is based on Redux, so we trigger state sync from server app to client app via Redux's dispatch.

workflow

  1. The user triggers the client app proxy method through DOM events
  2. This proxy method is executed on the server app.
  3. The server app state is synchronized back to the client app.

Example

The overall workflow of the reactant-share is shown in the figure below. Here is an example of a shared-worker type counter app.

  • First, we define a counter app module and view module in app.view.tsx
import React from "react";
import {
ViewModule,
createApp,
injectable,
useConnector,
action,
state,
delegate,
} from "reactant-share";

@injectable({ name: "counter" })
class Counter {
@state
count = 0;

@action
increase() {
this.count += 1;
}
}

@injectable()
export class AppView extends ViewModule {
constructor(public counter: Counter) {
super();
}

component() {
const count = useConnector(() => this.counter.count);
return (
<button type="button" onClick={() => delegate(this.counter, "increase", [])}>
{count}
</button>
);
}
}
  • Next, we use createSharedApp() to create the client app, whose options must contain workerURL, the worker url that will create a shared worker (if it hasn't been created yet).
import { render } from "reactant-web";
import { createSharedApp } from "reactant-share";
import { AppView } from "./app.view";

createSharedApp({
modules: [],
main: AppView,
render,
share: {
name: "SharedWorkerApp",
port: "client",
type: "SharedWorker",
workerURL: "worker.bundle.js",
},
}).then((app) => {
// render only
app.bootstrap(document.getElementById("app"));
});
  • Finally, we just create the worker file worker.tsx and build it as worker.bundle.js for the workerURL option.
import { createSharedApp } from "reactant-share";
import { AppView } from "./app.view";

createSharedApp({
modules: [],
main: AppView,
render: () => {
//
},
share: {
name: "SharedWorkerApp",
port: "server",
type: "SharedWorker",
},
}).then((app) => {
// render less
});

The specific workflow of increase looks like this.

  1. The user clicks the button in client app.
  2. delegate(this.counter, "increase", []) will be executed, which passes the parameters about the proxy execution to the server app.
  3. The server app will execute this.counter.increase(), and sync the updated state back to each client apps.

delegate() in reactant-share is inspired by the actor model.

reactant-share Framework

Multiple modes

  • Shared tab - It is suitable for running in browsers that do not support SharedWorker. The server app is an instance with rendering that also runs in a browser window. In multiple browser windows, there is also only one server app, and after it is closed or refreshed, one instance of the other client apps will be converted to a server app.
  • SharedWorker - If there is no browser compatibility requirement, reactant-share is highly recommended to use this mode, and reactant-share also does a graceful degradation, so if the browser does not support SharedWorker then the app will run in Shared-Tab mode.
  • Detached window - reactant-share allows sub-applications to run as Detached windows or to be quickly merged into a more complete application.
  • iframe - reactant-share allows each child application to run on an iframe.

Example repo: SharedWorker/Detached window/iframe

User Experience

Since reactant-share's multiple instances are logic-sharing and state-sharing, when a user opens the same reactant-share application in multiple browser windows, the only instance that is actually running in full is the server app.

The rendering-only client app will be so smooth that it will almost never freeze due to JS code, and the consistent application state will allow users to switch between multiple browser windows without any worries.

Development Experience

reactant-share provides CLI and full support for Typescript, as well as support for Shared-Tab, SharedWorker, and other different types of runtime modes out of the box. Built-in testbed for module testing, Routing and Persistence modules, and module dynamics support for lazy loading of reactant-share applications.

Service Discovery / Communications

Since reactant-share uses data-transport, reactant-share supports almost all the transports supported by data-transport.The client app and the server app, whichever is loaded first, the client app will wait for the server app to finish starting and get all the initial application state from it.

Using the actor model in the client app to design delegate(), we can do delegate(counterModule, 'increase', []) to let the server app proxy the execution of the module method and respond and sync both the state and the result back to the client app.

But if we need direct communication between the client app and the server app, then we need to use the PortDetector module.

class Counter {
constructor(public portDetector: PortDetector) {
this.portDetector.onServer(async (transport) => {
const result = await transport.emit("test", 42);
// result should be `hello, 42`
});
this.portDetector.onClient((transport) => {
transport.listen("test", (num) => `hello, ${num}`);
});
}
}

Tracking/Debugging

Since reactant-share is based on Redux, it fully supports Redux DevTools, and the immutable time travel that Redux brings will make debugging easy.

Fault Tolerance / Data Consistency

Since state synchronization after the client app uses delegate() to get the server app proxy to execute each time may cause it to be out of order in edge cases for various reasons, reactant-share integrates reactant-last-action, which provides sequence markers to keep If the client app receives a synchronized action that checks for an exception in the sequence, the client app will launch a full state synchronization to correct the action sequence.

In addition, when the browser does not support the Worker API, reactant-share will perform a graceful degradation (e.g. SharedWorker mode -> Shared-Tab mode -> SPA mode).

Isolation

Regardless of modes such as Shared-Tab, SharedWorker, each application instance runs in isolation and their basic interactions can only be triggered by delegate() to synchronize state.

Configuration

reactant-share provides CLI, you just need to run npx reactant-cli init shared-worker-example -t shared-worker to get a project of reactant-share with SharedWorker mode. If you want to change its mode, you just need to change the configuration of createSharedApp().

createSharedApp({
modules: [],
main: AppView,
render,
share: {
name: 'ReactantExampleApp',
port: 'client',
- port: 'client',
- type: 'SharedWorker',
- workerURL: 'worker.bundle.js',
+ type: 'SharedTab',
workerURL: 'worker.bundle.js',
},
}).then((app) => {
app.bootstrap(document.getElementById('app'));
});

With that, we can quickly turn SharedWorker mode into SharedTab mode.

Transport/Performance

Since the client app only renders and receives synchronized state. So the client app keeps running smoothly when the size of each dispatch update state does not exceed 50M. reactant uses Mutative Patch to update, usually this patch will be very small and reactant also does DEV checking for patch minimization updates. In fact, in most scenarios, the patch will not be that large.

Update state sizeVolume of dataDeserialization
30 Array * 1,000 items1.4 M14 ms
30 Array * 1,0000 items14 M130 ms
1000 Array * 1,000 items46 M380 ms

Notebook: 1 GHz Intel Core M / 8 GB 1600 MHz DDR3

benchmarking of the reactant-share module with derived data cache

Number of modules and statesTotal number of statesEach state update
100 modules * 20 states2,0003 ms
200 modules * 30 states6,0009 ms
300 modules * 100 states30,00044 ms

Notebook: 1 GHz Intel Core M / 8 GB 1600 MHz DDR3

Therefore, reactant-share still performs well in large projects.

Complexity

Whether it's practicing clean architecture, DDD, OOP or even FP, reactant-share has more openness to architect highly complex projects at will. reactant-share provides a few optional features, but the only one that shouldn't be missed is DI. reactant-share's DI is inspired by Angular, and it is very similar to Angular's DI. The complexity of coding that comes with architectural design is often determined by the final specification of the practice, but reactant-share hopes to help with such complex architectural design at the framework level.

Security

For reactant-share applications, the communication between server/client only serializes and deserializes state and parameters, so it is almost impossible to cause framework-level security issues. Of course, enabling https and usingSubresource Integrity are both necessary for any project that values front-end security, and we should also be concerned about XSS security in React documentation.

Testing

reactant-share provides testBed() to facilitate module testing. For example,

const { instance } = testBed({
main: Counter,
modules: [],
});

For integration testing of server app/client app interactions, reactant-share also provides mockPairTransports() for mock transport.

const transports = mockPairTransports();

createSharedApp({
modules: [],
main: AppView,
render,
share: {
name: "SharedWorkerApp",
port: "client",
type: "SharedWorker",
transports: {
client: transports[0],
},
},
}).then((app) => {
const clientApp = app;
// render only
app.bootstrap(document.getElementById("app"));
});

createSharedApp({
modules: [],
main: AppView,
render: () => {
//
},
share: {
name: "SharedWorkerApp",
port: "server",
type: "SharedWorker",
transports: {
client: transports[1],
},
},
}).then((app) => {
const serverApp = app;
// render less
});

After mocking transport like this, clientApp and serverApp can be easily tested for integration.

APIs

  • @injectable()

You can use @injectable() to decorate a module that can be injected and then use the emitDecoratorMetadata using TypeScript, or @inject() to inject the dependency.

  • @state

@state is used to decorate a class property that will create a reducer for Redux.

  • @action

It updates the redux state with mutations via the class method.

class Todo {
@state
list: { text: string }[] = [];

@action
addTodo(text: string) {
this.list.push({ text });
}
}
  • ViewModule/useConnector()

ViewModule is a view module with a component, which is completely different from React class component. The component of ViewModule is a function component that is used for the state connection between the module and the UI (using useConnector()) and for the application view bootstrap.

  • delegate()

delegate() transfers class methods execution from the client app to the server app and synchronizes the state to all client apps. It is inspired by the Actor model, but unlike other actor models, reactant-share's delegate() does not create new threads.

  • createSharedApp()

reactant-share supports multiple modes, and you can use createSharedApp() to create multiple different Shared Web Apps that interact with each other via transport APIs.

Q&A

  • Can reactant-share completely solve the complexity of the architecture?

Although reactant-share tries to reduce some complexity at the framework level, the complexity of large applications does not depend entirely on the framework itself, so even using reactant-share to architect a large project does not completely guarantee that it is absolutely clean, efficient, and maintainable. It involves testing strategy, code specification, CI/CD, development process, module design, and many other point.

But in terms of module model and shared model, reactant-share already provides as clean a design as possible. If you are interested in reactant-share, you can try it quickly.

  • Does reactant-share have no cons at all? Are there any limitations to using it?

reactant-share is a framework for building Shared Web Apps. But such a model is not free, and it will face performance issues with data transfer (The high maintenance cost of the SharedArrayBuffer has forced us to abandon it for now as well. In fact this is a problem caused by the fact that JS "multithreading" does not share memory efficiently).

Although Shared Web Apps lets the client App run in a render-only client thread, it introduces the additional overhead of synchronous state transfer. We must ensure that it is lightweight and efficient enough. While reactant-share does state patch based on Mutative, it is always difficult to ensure that each patch is minimally updated.

reactant-share provides a development option enablePatchesChecker. In development mode, it is enabled by default. Any mutation operation that is not a valid mutation will be alerted, usually eliminating the alert, and reactant-share will try to keep the update size as minimal as possible.

Conclusion

Front-end frameworks and architectures are always evolving. With full Worker support in modern browsers and an increasing number of multi-core CPU devices, we have reached a mature stage in our exploration of some multi-threaded running Web Apps. We have reasons to believe that the future Web App will be designed with lower complexity and run smoothly with multiple threads. It can fully utilize the user's device resources and give the user a good experience, and the developer does not need to have too many multi-threaded programming burden.

This is what reactant-share wants to try and work on.

If you think reactant-share is interesting, feel free to give it a star.

Repo: reactant

· 7 min read

Motivation

React is a JavaScript library for building user interfaces, but when we want to develop applications based on React, we often have to do a lot of building configuration and many other libraries' choices(Picking and learning a React state library and router library, etc.). We also need to consider how our business logic should be abstracted and structured. Everyone who uses React practices their own perception of how React is built, but it doesn't allow us to quickly focus on the business logic itself. As the application business grows in size, we urgently need a framework that can be easily understood and maintained.

And for the structured design of the application's business logic, separation of concern is a good idea. It requires clear boundaries of liability to avoid low maintainability when UI logic and business logic are mixed. We always want to focus on business logic when building applications. It is one of the business core values of an application. We want it to be easy to maintain, and test. Redux remains the most popular state library in React. It is full accord with immutable principles for React. Redux is just a state container, and we're often at a loss for how to really manage those states. We need a framework for scalable, loosely coupled and easily maintainable React applications.

React is an excellent UI library, but even if React has hooks, it's still not enough to solve all the problems we have in developing large applications. We still don't have module dependency injection, we don't have a good AOP practice model, we don't have a good abstraction possibility to minimize the module system, we also can't better practice DDD, and so on. These are all issues beyond React that we need to think about and solve.

Of course, I'm not going to discuss whether React needs to provide these features, it's good enough as it is. What is really being discussed is: Do we need a React framework?

In order to solve these problems, Reactant was created. It's a framework for React.

Introducing Reactant

Reactant efficiently builds extensible and maintainable React applications. Reactant is based on TypeScript, but it supports both TypeScript and JavaScript (for better development experience, TypeScript is recommended). Reactant provides dependency injection, modular model, immutable state management, module dynamization, and more. It is pluggable and highly testable. Not only is it able to quickly build a React application (Web and Native Mobile), it also brings some new React development experiences. With Reactant, you can still embrace OOP, FP, FRP, and other programming paradigms, and you can still embrace the entire React ecosystem.

Reactant is inspired by quite a few good features of Angular, for example, Reactant provides a similar dependency injection API to Angular. But Reactant is not a copy of Angular programming ideas on the React framework, Reactant provides fewer and more concise API, It is sufficient for all programming scenarios for developing applications.

It is a complete architecture of React.

What problem was solved?

Reactant is a progressive framework. In the process of developing applications from simple to complex, it can provide the appropriate features at each stage, based on its system architecture design can also be a variety of gradual and smooth upgrade and evolution.

Better Immutable State Management

React advocates immutable state type management, and Redux clearly fits this. But the fact is that simple mutation update operations like MobX are increasingly in line with current trends. Therefore Reactant provides a new immutable state management model based on Redux and Mutative, which incorporates similar API elements of MobX. And more importantly, it still maintains the immutability of state.

@injectable()
class TodoList {
@state
list: Todo[] = [];

@action
addTodo(text: string) {
this.list.push({ text, completed: false });
}
}

Modularization

While it seems that the entire React community is increasingly pushing functional programming after React introduced Hooks, functional programming may not be the best solution in complex enterprise businesses. Of course, Hooks does bring good solutions for decoupling rendering logic, if only in building UI. But in the realm of business logic, we have better options, especially in an enterprise application where multiple developers collaborate on development, and indeed class-based module design often brings parallel development and ease of maintenance and testing. class aren't evil, it's the wrong module design that's evil.

Therefore, Reactant advocates the use of classes for module implementation. And more importantly, Reactant defines Service Module, View Module, Plugin Module, so that their responsibilities and boundaries are more clearly defined. Any module can be a Service Module, it is flexible, and the architecture of many different applications can be based on it; View Module must define the view component bound to the current module, it is the rendering entry point for the view module, and the state of the modules it depends on will be injected intuitively into Props via useConnector; Plugin Module is a complete Redux middleware and Context re-encapsulation, it provides a model for designing plug-ins, which makes the plug-in API simplicity is possible.

In addition, Reactant provides a complete dependency injection API. It implements DI based on TypeScript decorator metadata, making it particularly easy to use.

@injectable()
class AppView extends ViewModule {
constructor(public counter: Counter) {
super();
}

component() {
const count = useConnector(() => this.counter.count);
return (
<button
type="button"
onClick={() => this.counter.increase()}>
{count}
</button>
);
}
}

Easy and Lightweight

Reactant has no more than 30 APIs and even fewer than 15 core APIs. Without much familiarity and adaptation, you can quickly get started with Reactant and use it to develop any complex React application.

The gzipped Reactant core code file is less than 50KB at runtime. Reactant not only supports code splitting, but it also supports the dynamic injection of modules, which is very useful for many large applications to run minimally.

Embracing React Ecosystem

Reactant is open, it abstracts some models based on React and Redux. These APIs bring convenience to developers, it also supports the ecosystem of React and Redux. Many superb third-party libraries can be used directly on Reactant or re-encapsulated, which brings infinite possibilities to the use of Reactant.

Better Development Experience

Reactant provides a simpler routing module (reactant-router) and a persistence module (reactant-storage). If necessary, you can develop any module based on the Reactant plug-in module you need a better module API.

In development debugging, devOptions supports both autoFreeze and reduxDevTools options. When enable autoFreeze, any changing operation without the @action decorated method will throw errors. And when enable reduxDevTools, Reactant will activate the support for Redux DevTools.

Reactant will do more features that improve the development experience.

Benchmark Performance

In benchmark performance tests between Reactant and MobX+React, Reactant has the edge in startup time and derived computing, while MobX+React has the edge in value updates. And overall, the performance difference is not particularly significant. Because Reactant is based on Mutative, Reactant also provides a performance-optimized solution when encountering a very few extreme performance bottlenecks.

Reactant is committed to maintaining good performance while continuing to build a productive React framework.

Conclusion

Reactant was originally intended to help React developers be able to efficiently build and develop a maintainable and testable application. Reactant's goal is to minimize the system life cost and maximize developer productivity.

As a brand new framework, Reactant has only been in development for a few months and there is still a lot of work to be done, including the build tools, development tools, server-side rendering, and the React Native CLI, and so on.

If you are already familiar with React, then you just need to quickly read the Reactant part of the documentation and use the Reactant CLI to quickly build the Reactant application, you will start your new React application development experience.

Repo:

https://github.com/unadlib/reactant