npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ringcentral-integration/next-core

v0.0.1

Published

Core package for RingCentral Integration next-generation architecture.

Downloads

178

Readme

@ringcentral-integration/next-core

This is the foundation package for RingCentral integration products, it is based on Reactant. Reactant is a progressive framework based on Redux, Inversify, React Router, and Mutative for OOP module model design.

It supports building shared web applications that support multiple windows.

  • Shared Tab
  • Shared Worker
  • Detached window
  • iframe
  • Any other data-transport based application port, such as WebRTC

Table of Contents

Usage

yarn add @ringcentral-integration/next-core
const app = await createApp(config);
app.bootstrap(document.getElementById('app'));

APIs

Dependency Injection

The foundation package provides dependency injection, which supports TypeScript(also supports JavaScript), and we recommend the experimentalDecorators feature based on TypeScript, as well as Reflect.metadata to record the dependency injection metadata.

If using babel, make sure that you install @babel/plugin-propose-decorators, babel-plugin-transform-typescript-metadata and configure the babel settings correctly.

To enable experimental support for decorators, you must enable the experimentalDecorators and emitDecoratorMetadata compiler option either on the command line or in your tsconfig.json:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

@injectable()

Use @injectable(), it will turn the current module into an injectable module.

@injectable({
    name: 'Foo',
})
class Foo extends RcModule {}

@injectable({
    name: 'Bar',
})
class Bar extends RcModule {
    constructor(protected foo: Foo) {
        super();
    }
}

If it does not depend on any other module, in fact, the @injectable() modifier of the current module can be omitted, and turns it into an injectable module and injects it automatically when the createApp() runs.

@inject()

Use @inject() and bring its corresponding identifier parameter as dependency injection module.

If you only inject the class itself as a dependency identifier, then you can use protected bar: Bar.

interface Bar {
    text: string;
}

@injectable({
    name: 'Foo',
})
class Foo extends RcModule {
    constructor(@inject('Bar') protected bar: Bar) {
        super();
    }

    get text() {
        return this.bar.text;
    }
}

@optional()

Use optional() with a dependency identifier that you can use to inject an optional module.

If you only need the class itself as a dependency identifier, then you can abbreviate @optional(Bar) protected bar?: Bar to @optional() protected bar?: Bar.

interface Bar {
    text: string;
}

@injectable({
    name: 'Foo',
})
class Foo extends RcModule {
    constructor(@optional('Bar') public bar?: Bar) {
        super();
    }

    get text() {
        return this.bar?.text;
    }
}

Optional modules are not automatically injected by default. If you need it, please specify the injected module on createApp().

ModuleRef

If the app have circular dependencies, you can define a getter in this module and use ModuleRef to dynamically obtain an instance of the dependency.

@injectable({
    name: 'Bar',
})
class Bar extends RcModule {
    constructor(public foo: Foo) {
        super();
    }
}

@injectable({
    name: 'Foo',
})
class Foo extends RcModule {
    constructor(public moduleRef: ModuleRef) {
        super();
    }

    get bar() {
        return this.moduleRef.get(Bar);
    }
}

RcModule APIs

@ringcentral-integration/next-core provides RcModule base module, decorators @state, @action, @computed.

  • onInit()

onInit lifecycle for current initialization after all deps modules are all ready.

  • onInitOnce()

onInitOnce once lifecycle for current initialization after all deps modules are all ready.

  • onInitSuccess()

onInitSuccess lifecycle for current initialization after this module is ready.

  • onReset()

onReset lifecycle for current reset after one of deps modules is not ready.

The first full lifecycle is onInitOnce() -> onInit() -> "this.ready === true" -> onInitSuccess(). And The re-login lifecycle is logout -> onReset() -> login -> onInit() -> "this.ready === true" -> onInitSuccess().

If the current module has persistent state, then the full lifecycle can only start when the hydration is completed.

For example:

import {
    RcModule,
    state,
    action,
    computed,
} from '@ringcentral-integration/next-core';

class Auth extends RcModule {
    constructor(protected _client: Client) {
        super();
    }

    @state
    connected = '';

    @action
    changeConnection(connected) {
        this.connected = connected;
    }

    async connect() {
        await this._client.connect();
        this.changeConnection(true);
    }

    @computed((that: Auth) => [that.connected])
    get permissions() {
        return { writeable: this.connected, readable: true };
    }

    override async onInitSuccess() {
        //
    }
}

@state

@state is used to decorate a module state, which is based on the Redux reducer.

  • that will be an state in redux, and never be clear after some view be destroyed.
  • if you want that state only inside some components, should use useState from React.

@action

@action is used to decorate a method that changes the state of the module (Executing it will dispatch a Redux action), and it does NOT support asynchronous methods.

The method decorated with @action in the current module CAN call the method decorated with @action in other modules.

  • The @action decorated method should have no side effects.
@injectable({
    name: 'ContactsList',
})
class ContactsList extends RcModule {
    constructor() {
        super();
    }

    @state
    contacts: Contact[] = [];

    @action
    addContacts(contacts: Contact[]) {
        // ❌ bad practice
        contacts.forEach(() => {
            // fetch avatar
        });
        // ....
    }

    override onInitOnce() {
        // ✅ good practice
        watch(
            this,
            () => this.contacts,
            () => {
                // fetch avatar
            },
        );
    }
}
  • The state operations in the methods decorated by @action should be *mutation updates as possible to ensure patch minimization.
@injectable({
    name: 'ContactsList',
})
class ContactsList extends RcModule {
    constructor() {
        super();
    }

    @state
    contacts: Contact[] = [];

    @action
    addContact(contact: Contact) {
        // ❌ bad practice
        this.contacts = [...this.contacts, contact];
    }

    @action
    addContact(contact: Contact) {
        // ✅ good practice
        this.contacts.push(contact);
    }
}

@action supports calling other actions that execute other update states and merge updates into the UI (They must be the current module state, cross-module states will still be updated one by one).

@computed()

@computed supports automatic collection of dependencies and automatic re-computing when the state of a dependency changes.

Note: Automatic collection of dependent target properties is only supported for properties decorated by @state and getter fields decorated by @computed.

If you want to manage dependencies manually then, you can use @computed(callback) for a getter, you should make sure that the return value of its callback function is an Array of dependency collections.

@injectable({
    name: 'Auth',
})
class Auth extends RcModule {
    constructor() {
        super();
    }

    @state
    connected = '';

    @state
    readable = false;

    @computed
    // or you can also manage dependencies manually like this:
    // @computed((that: Auth) => [that.connected, that.readable])
    get permissions() {
        return {
            writeable: getWriteable(this.connected),
            readable: this.readable,
        };
    }
}

Note: The @computed decorator only supports getter fields, and the getter fields must be readonly. During the execution of a getter with @computed, no @action methods should be invoked.

delegate

The delegate() supports both function calls and being used as a decorator.

To maintain flexibility in the execution of delegates, we recommend giving priority to using function calls.

  1. Using function:
  • You can simply delegate usage like this: await delegate(this.counter, 'increment');, with the default delegation target being 'server'.
  • If you want to include the arguments of the function being delegated, then you can use it like this: await delegate(this.counter, 'decrement', [1]);.

By defining the 'target' options of delegate(), we can specify our execution target. The following targets are supported:

  • await delegate(this.counter, 'increment', [], { target: 'server' });

It is equivalent to await delegate(this.counter, 'increment'). Delegate execution, run that method in server. Unless the target is 'server', there are no more options available.

  • delegate(this.counter, 'increment', [], { target: 'all' });

Parallel execution, run that method in all clients and server.

  • delegate(this.counter, 'increment', [], { target: 'clients' });

Parallel execution, run that method in all clients.

  • await delegate(this.counter, 'increment', [], { target: 'mainClient' });

Delegate execution in main client.

  1. Using decorator:
  • @delegate('server')

Delegate execution, run that method in server.

  • @delegate('all')

Parallel execution, run that method in all clients and server.

  • @delegate('clients')

Parallel execution, run that method in all clients.

  • @delegate('mainClient')

Delegate execution in main client.

logger

The logger is a built-in logger that supports info, warn, error, debug, and trace methods.

@injectable({
    name: 'Counter',
})
class Counter extends RcModule {
    constructor() {
        super();
    }

    @state
    count = 0;

    @action
    increment() {
        this.count += 1;
        this.logger.info('increment', this.count);
    }
}

RcViewModule APIs

We can be used to implement a module with a View by inheriting the RcViewModule and defining the component method (a React function component).

It is possible to inject any method of the current RcViewModule in its component, and also to inject the current shared module state or other dependent module state using useConnector() React hook:

interface Todo {
    text: string;
    completed: boolean;
}

@injectable({
    name: 'TodoView',
})
class TodoView extends RcViewModule {
    @state
    list: Todo[] = [];

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

    @action
    toggleTodo(key: number, value: boolean) {
        this.list[key].completed = !value;
    }

    component() {
        const list = useConnector(() => this.list);
        return (
            <ul>
                {this.list.map(({ text, completed }, key) => (
                    <li
                        key={key}
                        onClick={() => this.toggleTodo(key, completed)}
                    >
                        {text}
                    </li>
                ))}
            </ul>
        );
    }
}

useConnector also supports returning a state object, which automatically makes shallow comparisons:

const { list, visibilityFilter } = useConnector(() => ({
    list: this.list,
    visibilityFilter: this.visibilityFilter,
}));

It should be noted that while the ViewModule supports inheritance, function components based on component method implementations must be called in the same way as components based on superclass component methods, not using the jsx:

@injectable({
    name: 'BaseFooView',
})
class BaseFooView extends RcViewModule {
    component() {
        return <span>foo</span>;
    }
}

@injectable({
    name: 'FooView',
})
class FooView extends BaseFooView {
    component(props) {
        return (
            <>
                <span>foo</span>
                {
                    super.component(props)
                    // Don't make it: <super.component />
                }
            </>
        );
    }
}

State Subscription APIs

watch()

You can use watch() to observe a specific state changes in any class module.

For example,

@injectable({
    name: 'Counter',
})
class Counter extends RcModule {
    constructor() {
        super();
        watch(
            this,
            () => this.count.sum,
            (newValue, oldValue) => {
                //
            },
        );
    }

    @state
    count = { sum: 0 };
}

You can pass the option { multiple: true }, which will support watching multiple values.

For example,

@injectable({
    name: 'Counter',
})
class Counter extends RcModule {
    constructor() {
        super();
        watch(
            this,
            () => [this.count0, this.count1] as const,
            ([newCount0, newCount1], [oldCount0, oldCount0]) => {
                //
            },
            {
                multiple: true,
            },
        );
    }

    @state
    count0 = 0;

    @state
    count1 = 0;
}

watch option supports passing in { isEqual: () => {} } function for custom equal.

subscribe()

You can use subscribe() to subscribe state changes in any class module.

For example,

@injectable({
    name: 'Counter',
})
class Counter extends RcModule {
    constructor() {
        super();
        subscribe(this, () => {
            //
        });
    }

    @state
    count = { sum: 0 };
}

Storage APIs

@storage

You can use @storage to decorate a persistent state, and execute enable(instance) to enable the ability of persistence. When the account logouts, this persistence state will be cleared.

@injectable({
    name: 'Counter',
})
class Counter extends RcViewModule {
    constructor(public storage: StoragePlugin) {
        super();
        this.storage.enable(this);
    }

    @storage
    @state
    sum = 0;

    @action
    increment() {
        this.sum += 1;
    }
}

@userStorage

If you want the persistent state not to be cleared when the current account logouts, you can use @userStorage to decorate a persistent state.

@injectable({
    name: 'Counter',
})
class Counter extends RcViewModule {
    constructor(public storage: StoragePlugin) {
        super();
        this.storage.enable(this);
    }

    @userStorage
    @state
    sum = 0;

    @action
    increment() {
        this.sum += 1;
    }
}

You can use storage.purge() for clear all storage.

Router APIs

The foundation package provides a router based on React Router v5.3.1, and you can use the Router module to define the routing configuration.

const app = await createSharedApp({
    modules: [
        RouterPlugin,
        {
            provide: RouterOptions,
            useValue: {
                createHistory: () => createBrowserHistory(),
            } satisfies IRouterOptions,
        },
    ],
    main: AppView,
    render,
});

History is a dependency of the router, and you can use createBrowserHistory, createHashHistory or createMemoryHistory to create a history object. For detail information, please refer to the React Router and history documentation.

createSharedApp()

The createSharedApp() method is used to create a shared application, and you can use it to create a shared application that supports multiple windows or Base mode app.

const app = await createSharedApp({
    modules: [
        RouterPlugin,
        {
            provide: RouterOptions,
            useValue: {
                createHistory: () => createBrowserHistory(),
            } satisfies IRouterOptions,
        },
    ],
    main: AppView,
    render,
    share: {
        name,
        type: 'Base', // 'Base' | 'SharedTab' | 'SharedWorker'
    },
});

If you want to create a shared application that supports multiple windows, you can use the SharedWorker type, When the browser does not support SharedWorker, then the program will automatically downgrade to setting SharedTab.

i18n

The foundation package provides an i18n hooks, and you can use the useLocale() React hook to get the t function.

import { useLocale } from '@ringcentral-integration/micro-core/src/app/hooks';

import i18n from './i18n';

const { t } = useLocale(i18n);
const text = t('loginSuccess');

PortManager

If you want to do the multiprocessing programming, you can use the PortManager module to manage the ports. You can read the PortManager public API for more information.

You can use it to manage the logic for different types of ports:

  • On the server side, you can use onServer to manage server-side port logic.
  • On the client side, you can use onClient to manage client-side port logic.
  • On the main tab side, you can use onMainTab to manage main tab port logic.

For example:

@injectable({
    name: 'App',
})
class App extends RcModule {
    constructor() {
        super();

        if (this._portManager.shared) {
            this._portManager.onServer(() => this.initializeOnServer());
            this._portManager.onClient(() => this.initializeOnClient());
        } else {
            this.initializeOnServer();
            this.initializeOnClient();
        }
    }
}

Debugging

You can use devOptions for debugging, and you can use devOptions to enable the Redux DevTools and Module Lifecycle Check.

const app = await createSharedApp({
    modules,
    main: AppView,
    render,
    share,
    devOptions, // enable devOptions
});

This is the DevOptions interface:

interface DevOptions {
    /**
     * Enable strict mode for React and Mutative.
     */
    strict?: boolean;
    /**
     * Enable state update patches.
     */
    enablePatches?: boolean;
    /**
     * Enable inspector for state changing.
     */
    enableInspector?: boolean;
    /**
     * Enable state auto freeze.
     */
    autoFreeze?: boolean;
    /**
     * Enable auto computed.
     */
    autoComputed?: boolean;
    /**
     * Enable redux dev tools.
     */
    reduxDevTools?: boolean;
    /**
     * Redux dev tools enhancer options.
     */
    reduxDevToolsOptions?: ReduxDevToolsOptions;
}

Module Lifecycle Check

You can use checkModules() to check the lifecycle of all the module, And you can use checkModule(module) to check the lifecycle of a specific module.

Module Performance Track

If you want to track the performance of the module initialization, you can use trackModule() to track the performance of all the module.

Plugin Module

Pluggable is an important concept of the f. It provides an interface that allows you to encapsulate many of React's libraries in a clean design. You can base complex plugins on it to encapsulate them simply enough.

@injectable({
    name: 'ExamplePlugin',
})
class ExamplePlugin extends PluginModule {
    constructor() {
        super();
    }
}

For detail information, please refer to the Pluggable API docs

Dynamic Module

You can use @dynamic() to define a dynamic module, and then use load() to load the lazy module.

@injectable({
    name: 'Klazz',
})
class Klazz extends RcModule {
    @dynamic('LazyLoad')
    lazyLoad?: LazyLoad;

    constructor() {
        super();
    }

    async lazyLoadExample() {
        const LazyLoad = await import('./LazyLoad').then((m) => m.LazyLoad);

        await load(this, [
            {
                provide: 'LazyLoad',
                useClass: LazyLoad,
            },
        ]);

        console.log(this.lazyLoad);
    }
}

Abstract class

You can use abstract to define an abstract class without @injectable(), and then use @injectable() to decorate the subclass.

abstract class Klazz extends RcModule {
    constructor() {
        super();
    }
}

And you can use @injectable() to decorate the subclass.

@injectable({
    name: 'Foo',
})
class Foo extends Klazz {
    constructor() {
        super();
    }
}

Subclass

You can use extends to define a subclass, and then use @injectable() to decorate the subclass.

@injectable({
    name: 'Klazz',
})
class Klazz extends RcModule {
    constructor(public foo: Foo) {
        super();
    }
}

@injectable({
    name: 'Foo',
})
class Foo extends Klazz {
    constructor(public foo: Foo) {
        // !!! we need to call super(foo) to pass the foo to the parent class
        super(foo);
    }
}