GithubHelp home page GithubHelp logo

wix-incubator / repluggable Goto Github PK

View Code? Open in Web Editor NEW
166.0 143.0 13.0 1.9 MB

Pluggable micro frontends in React+Redux apps

License: MIT License

TypeScript 98.97% JavaScript 1.03%
frontend framework dependency-injection typescript micro-frontends inversion-of-control

repluggable's Introduction

Repluggable

master build npm version

Repluggable is a library that's implementing inversion of control for front end applications and makes development of medium or high-complexity projects much easier. Currently Repluggable implements micro-frontends in a React+Redux app, with plans to make it framework-independent in the future.

Functionality of a Repluggable app is composed incrementally from a list of pluggable packages. Every package extends the already loaded ones by contributing new functionality into them. Sections of UI, contributed by a certain package, can be rendered anywhere and are not limited to dedicated subtree of DOM. All packages privately manage their state in a modular Redux store, which plays the role of common event mechanism. Packages interact with each other by contributing and consuming APIs, which are objects that implement declared interfaces. Packages can be plugged in and out at runtime without the need to reload a page. Check out the architecture section of the docs to learn more about the design decisions behind Repluggable.

Quick docs links: How-to | Architecture and core concepts

Getting started

All code in this README is in TypeScript.

Installation

To add Repluggable to an existing React+Redux application:

$ npm install repluggable

Create a new Repluggable project

Run the following commands:

npx create-react-app your-app-name --template typescript
cd your-app-name
yarn add [email protected] [email protected] @types/[email protected] @types/[email protected] repluggable
rm src/App*
rm src/logo*
cp -R node_modules/repluggable/examples/helloWorld/src/ ./src
yarn start

Writing a pluggable package

A pluggable package is basically an array of entry points. An entry point is an object which contributes certain functionality to the application. Below is an example of a simple entry point.

foo.ts

import { EntryPoint } from 'repluggable'

export const Foo : EntryPoint = {
    name: 'FOO',

    attach() {
        console.log('FOO is here!')
    }
}

Usually, a pluggable package will be a separate npm project, which exports an array of entry points. But it isn't required - entry points can also be part of the main app.

Bootstrapping the main application

Main application is the React+Redux app that's being composed from packages. Suppose we also have bar.ts implemented similarly to Foo above.

App.tsx

import { createAppHost, AppMainView } from 'repluggable'
import { Foo } from './foo'
import { Bar } from './bar'

const host = createAppHost([
    // the list of initially loaded packages
    Foo,
    Bar
])

ReactDOM.render(
    <AppMainView host={host} />,
    document.getElementById('root')
)

When run, the application will print two messages to console, first 'FOO is here!', then 'BAR is here!'.

How-to

Developing main application

The main application is a React application, which uses the repluggable package.

The index.ts of the application must perform the following steps.

  1. Import from repluggable

    import { createAppHost, AppMainView } from 'repluggable'
  2. Provide loaders of pluggable packages. Below is an example of three packages, each loaded in a different way:

    • package-foo is statically bundled with the main app
    • package-bar is in a separate chunk (WebPack code splitting). We'll load it with dynamic import.
    • package-baz is in an AMD module, deployed separately. We'll load it with RequireJS.

    This is how the three packages are loaded:

    import foo from 'package-foo'
    const bar = () => import('package-bar').then(m => m.default)
    const baz = require('package-baz')
  3. Initialize AppHost with the packages:

    const host = createAppHost([
        foo,
        baz
    ])
    
    // Later
    void bar().then(p => host.addShells([p]))
  4. Render AppMainView component, passing it the host:

    ReactDOM.render(
        <AppMainView host={host} />,
        document.getElementById('root')
    )

Full code

import ReactDOM from 'react-dom'
import { createAppHost, AppMainView } from 'repluggable'

import packageOne from 'package-one'
const packageTwo = () => import('package-two').then(m => m.default)
const packageThree = require('package-three')

const host = createAppHost([
    packageOne,
    packageThree
])

ReactDOM.render(
    <AppMainView host={host} />,
    document.getElementById('root')
)

// Sometime later
void packageTwo().then(p => host.addShells([p]))

Developing a pluggable package

Creating a package project

A package project is a regular Node project.

Typically, it is set up with TypeScript, React, and Redux. Such a project must include a repluggable dependency.

The rest of the configuration (Babel, WebPack, Jest, etc) heavily depends on the organization of your codebase and release pipeline, and is outside the scope of this README.

Creating entry points

As we mentioned before, each package must export one or more entry points in order to be loaded by the main app.

An entry point is an object which implements an EntryPoint interface:

import { EntryPoint } from 'repluggable'

const FooEntryPoint: EntryPoint = {

    // required: specify unique name of the entry point
    name: 'FOO',

    // optional
    getDependencyAPIs() {
        return [
            // DO list required API keys
            BarAPI
        ]
    },

    // optional
    declareAPIs() {
        // DO list API keys that will be contributed 
        return [
            FooAPI
        ]
    },

    // optional
    attach(shell: Shell) {
        // DO contribute APIs
        // DO contribute reducers
        // DO NOT consume APIs
        // DO NOT access store
        shell.contributeAPI(FooAPI, () => createFooAPI(shell))
    },

    // optional
    extend(shell: Shell) {
        // DO access store if necessary
        shell.getStore().dispatch(....)
        // DO consume APIs and contribute to other packages
        shell.getAPI(BarAPI).contributeBarItem(() => <FooItem />)
    },

    // optional
    detach(shell: Shell) {
        // DO perform any necessary cleanup
    }
}

The EntryPoint interface consists of:

  • declarations: name, getDependencies()
  • lifecycle hooks: attach(), extend(), detach()

The lifecycle hooks receive a Shell object, which represents the AppHost for this specific entry point.

Exporting entry points

The default export of a package must be an array of its entry points. For example, in package root index.ts :

import { FooEntryPoint } from './fooEntryPoint'
import { BarEntryPoint } from './barEntryPoint'

export default [
    FooEntryPoint,
    BarEntryPoint
]

Creating an API

To create an API, perform these steps:

  1. Declare an API interface. For example:

    export interface FooAPI {
        doSomething(): void
        doSomethingElse(what: string): Promise<number>
    }
  2. Declare an API key, which is a const named after the interface, as follows:

    import { SlotKey } from 'repluggable'
    
    export const FooAPI: SlotKey<FooAPI> = {
        name: 'Foo API',
        public: true
    }

    Note that public: true is required if you plan to export your API outside of your package. The key must be declared in the same .ts file with the interface.

  3. Implement your API. For example:

    export function createFooAPI(shell: Shell): FooAPI {
        return {
            doSomething(): void {
                // ...
            },
            doSomethingElse(what: string): Promise<number> {
                // ...
            }
        }
    }
  4. Contribute your API from an entry point attach function:

    import { FooAPI, createFooAPI } from './fooApi'
    
    const FooEntryPoint: EntryPoint = {
    
        ...
    
        attach(shell: Shell) {
            shell.contributeAPI(FooAPI, () => createFooAPI(shell))
        }
    
        ...
    
    }
  5. Export your API from the package. For example, in the index.ts of your package:

    export { FooAPI } from './fooApi'

Managing state

In order to manage state in a package, you need to contribute one or more reducers.

In the example below, FooEntryPoint will contribute two reducers, bazReducer and quxReducer.

To contribute the reducers, perform these steps:

  1. Declare types that represent the state for each reducer:

    // state managed by bazReducer
    export interface BazState {
        ...
        xyzzy: number // for example
    }
    
    // state managed by quxReducer
    export interface QuxState {
        ...
    }
  2. Wrap these state types in a root state type. This root type determines the shape of the state in the entry point.

    // the root type on entry point level
    export interface FooState {
        baz: BazState
        qux: QuxState
    }
  3. Write the two reducers. For example, they can look like this:

    function bazReducer(
        state: BazState = { /* initial values */ },
        action: Action)
    {
         ...
    }
    
    function quxReducer(
        state: QuxState = { /* initial values */ },
        action: Action)
    {
         ...
    }
  4. Contribute state in the entry point:

    attach(shell: Shell) {
        shell.contributeState<FooState>(() => ({
            baz: bazReducer,
            qux: quxReducer
        }))
    }

    Here, an argument passed to contributeState() is a reducer map object. This object contains all the same keys of FooState (the baz and qux), but this time the keys are assigned their respective reducers. Such derivation of reducers' map shape is enforced by the typings.

  5. Expose selectors and action dispatchers through APIs:

    export interface FooAPI {
        ...
        getBazXyzzy(): number
        setBazXyzzy(value: number): void
        ...
    }

    The above API allows to read and change the value of xyzzy in the BazState.

    Note that neither of these two functions are passed the state or the Store object. This is because their implementations are already bound to the store of the AppHost:

    const createFooAPI = (shell: Shell): FooAPI => {
        // this returns a scoped wrapper over the full
        // store of the main application
        // IMPORTANT! the generic parameter (FooState)
        // must match the one specified when contributing state!
        // In our example, we did contributeState<FooState>(...)
        const entryPointStore = shell.getStore<FooState>()
    
        const getState = () => entryPointStore.getState()
    
        return {
            ...
            // example of selector
            getBazXyzzy(): number {
                const state: FooState = getState()
                return state.baz.xyzzy
            },
            // example of action dispatcher
            setBazXyzzy(value: number): void {
                entryPointStore.dispatch(BazActionCreators.setXyzzy(value))
            }
            ...
        }
    }

Creating React components

When creating a React component, we strongly recommend to follow the React-Redux pattern, and separate your component into a stateless render and a connect container.

In repluggable, components often need to consume APIs. Although APIs can be obtained through Shell, when it is passed to lifecycle hooks in your entry point, propagating them down the component hierarchy would be cumbersome.

A more elegant solution is to use connectWithShell() function instead of the regular connect(). This provides the connector with the ability to obtain APIs.

The example below illustrates how connectWithShell() is used. Suppose we want to create a component <Foo />, which would render like this:

(props) => (
    <div className="foo">
        <div>
            <label>XYZZY</label>
            <input
                type="text"
                defaultValue={props.xyzzy}
                onChange={e => props.setXyzzy(e.target.value)} />
        </div>
        <div>
            Current BAR = {props.bar}
            <button onClick={() => props.createNewBar()}>
                Create new BAR
            </button>
        </div>
    </div>
)

In order to implement such a component, follow these steps:

  1. Declare the type of state props, which is the object you return from mapStateToProps:

    type FooStateProps = {
        // retrieved from own package state
        xyzzy: string
        // retrieved from another package API
        bar: number
    }
  2. Declare type of dispatch props, which is the object you return from mapDispatchToProps:

    type FooDispatchProps = {
        setXyzzy(newValue: string): void
        createNewBar(): void
    }
  3. Write the stateless function component. Note that its props type is specified as FooStateProps & FooDispatchProps:

    const FooSfc: React.SFC<FooStateProps & FooDispatchProps> =
        (props) => (
            <div className="foo">
                ...
            </div>        
        )
  4. Write the connected container using connectWithShell. The latter differs from connect in that it passes Shell as the first parameter to mapStateToProps and mapDispatchToProps. The new parameter is followed by the regular parameters passed by connect. For example:

    export const createFoo = (boundShell: Shell) => connectWithShell(
        // mapStateToProps
        // - shell: represents the associated entry point
        // - the rest are regular parameters of mapStateToProps
        (shell, state) => {
            return {
                // some properties can map from your own state
                xyzzy: state.baz.xyzzy,
                // some properties may come from other packages' APIs
                bar: shell.getAPI(BarAPI).getCurrentBar()
            }
        },
        // mapDispatchToProps
        // - shell: represents the associated entry point
        // - the rest are regular parameters of mapDispatchToProps
        (shell, dispatch) => {
            return {
                // some actions may alter your own state
                setXyzzy(newValue: string): void {
                    dispatch(FooActionCreators.setXyzzy(newValue))
                },
                // others may request actions from other packages' APIs
                createNewBar() {
                    shell.getAPI(BarAPI).createNewBar()  
                }
            }
        },
        boundShell
    )(FooSfc)

    The Shell parameter is extracted from React context EntryPointContext, which represents current package boundary for the component.

Exporting React components

TBD (advanced topic)

Testing a package

TBD

Local development

HMR (hot module replacement)

For a smooth local development experience, it's recommended to enable HMR, allowing packages' code to update immediately without the need to reload the entire page. (For more information on HMR, see webpack's docs)

To enable HMR, use Repluggable's hot util to wrap the export of your repluggable package. For example, in the package root index.ts:

import { hot } from 'repluggable'
import { FooEntryPoint } from './fooEntryPoint'
import { BarEntryPoint } from './barEntryPoint'

export default hot(module, [
    FooEntryPoint,
    BarEntryPoint
])

Architecture

repluggable allows composition of a React+Redux application entirely from a list of pluggable packages.

Think of a package as a box of lego pieces - such as UI, state, and logic. When a package is plugged in, it contributes its pieces by connecting them to other pieces added earlier. In this way, the entire application is built up from connected pieces, much like a lego set.

For two pieces to connect, one piece defines a connection point (an extension slot) for another specific type of a piece. In order to connect, the other piece has to match the type of the slot. One slot can contain many pieces.

Packages can be plugged in and out at runtime. Contributed pieces are added and removed from the application on the fly, without the need for a page to reload.

Main application

This is the application being composed, much like a lego set. We refer to it as main application.

The main application can be as small as an empty shell. Its functionality can be completely composed by the packages, where each plugged package contributes its pieces to the whole.

The minimal responsibilities of the main application are:

  • Initialize an AppHost object with a list of pluggable packages.

    The AppHost object orchestrates lifecycle of the packages, handles cross-cutting concerns at package boundaries, and provides dependency injection to Redux-connected components.

  • Render AppMainView component, passing it the initialized AppHost in props.

Pluggable packages

Pluggable package (or simply package) is a regular Node package, which exports an array of entry points.

The packages are plugged in the order they are listed when passed to AppHost. Entry points are invoked in the list order of the packages, in the array order within the package.

Entry points

Every entry point contributes one or more pieces to the whole lego set of the application.

Examples of contributed pieces include React components, panel item descriptors, UI command descriptors, etc, etc. They can be anything, provided that they are expected by the "lego set". Here expected means that some package provides an API, through which it accepts contributions of this specific type.

There are also two kinds of contributions supported directly by repluggable: APIs and reducers.

Besides contributing lego pieces, entry points may contain additional lifecycle hooks.

APIs

Some packages (providers) provide services to other packages (consumers). The services are provided through APIs. An API is an object, which implements a TypeScript interface, and is identified by an API key. An API key is another object declared as a const TODO: link to example, and exported from the package.

In general, APIs allow packages to extend other packages (consumers call APIs, which let them pass contributions to the provider), and otherwise interact. Moreover, APIs are the only allowed way of interaction between packages.

In order to provide an API, a provider package:

  • declares and exports an API interface and an API key
  • implements an API object according to the interface
  • contributes an API object under the key

In order to consume an API, a consumer package:

  • imports an API key and an API interface from the provider package
  • declares dependency on the API in relevant entry points
  • retrieves an API object by calling getAPI and passing it the API key TODO: link to example.

Reducers

repluggable requires that all state of the application is managed in a Redux store. This ensures that all pieces are connected to a single event-driven mechanism. In turn, this guarantees that pure React components mapped to values returned by APIs will re-render once these values change.

A package that has state must contribute one or more reducers responsible for managing that state. If such a package contributes APIs, it can also include selectors and action dispatchers in the APIs.

The Redux store of the main application is combined from reducers, contributed by stateful packages.

Extension Slots

When a package accepts contributions from other packages, it must store contributed pieces in some kind of an array.

repluggable provides a "smart" array for this purpose, named extension slot. Extension slot is a generic object ExtensionSlot<T>, which accepts contributions of type T.

Its additional responsibility is remembering which package and entry point each contribution was received from. This allows applying package boundaries and easily handling other cross-cutting concerns.

Extension slots are implementation details of a package, and they should never be directly exposed outside of the package. Instead, a package:

  • internally initializes an extension slot for every kind or group of accepted contributions.
  • contributes an API that receives contributions from the outside and pushes them to an internal extension slot.

With that, the AppHost also tracks all existing extension slots. This approach allows for an easy implementation of application-wide aspects. For example, removal of a package with all of its contributions across an application.

Package boundaries in DOM

Every React component rendered under the AppMainView is associated with an entry point context.

The entry point context is a React context, which associates its children with a specific entry point, and thus the package that contains it.

Such association provides several aspects to the children:

  • performance measurements and errors reported by the children are automatically tagged with the entry point and the package

  • in Redux-connected components (TODO: link to details):

    • dependency injection (the getAPI function): all dependencies are resolved in the context of the entry point

    • state scoping (the state in mapStateToProps, and getState() in thunks): returned state object is scoped to reducers contributed by the entry point.

    TODO: verify that getState() in thunks is actually scoped

    • when rendering an extension slot of contributed React components: each component is rendered within the context of the entry point it was contributed by.

Progressive loading

To make application loading reliable and fast, repluggable allows flexible control over package loading process.

The loading process is abstracted from any concrete module system or loader. Packages can be in a monolith bundle, or loaded with dynamic imports, or with loaders like RequireJS. To add a package to an AppHost, all that's needed is a Promise of a package default export.

Packages can be added to an AppHost at different stages:

  • During initialization of the AppHost
  • Right after the AppMainView was rendered for the first time
  • Lazily at any later time

Moreover, AppHost allows separating the whole package into multiple entry points. Some of the entry points are added right as the package is added to the AppHost, while others can be added later.

Such separation allows incremental contribution of functional parts as they become ready. Some parts may need to dynamically load additional dependencies or request data from backends. Without the separation approach, a user won't be able to interact with any functionality of the package, until the entire package is initialized -- which would hurt the experience.

In addition, AppHost supports removal of previously added entry points or entire packages, at any time. Removal of a package means removal of all its entry points. When an entry point is removed, all contributions made from that entry point are removed too.

API dependencies

Since APIs are contributed though entry points, their availability depends on the loading time of the provider package and a specific entry point within it. From a consumer package perspective, this creates a situation in which one or more of the APIs the package depends on may be temporarily unavailable.

AppHost resolves that with the help of explicit dependency declarations. Every entry point must declare APIs which it's dependent on (including dependencies of all pieces contributed by the entry point). If any of the required APIs is unavailable, the entry point is put on hold. There are two possible cases:

  • Attempted to add an entry point, but some of required APIs weren't available: the entry point is put on hold, and will be added as soon as all required APIs are contributed.
  • An entry point was added, but then some of its required APIs became unavailable: the entry point will be removed together with all of its contributions, and put on hold. It will be added once again as soon as all required APIs are available.

Such approach guarantees that code dependent on an API from another package will not run unless that API is available.

Licenses

3-rd party licenses are listed in docs/3rd-party-licenses.md

repluggable's People

Contributors

amimagid avatar amiryonatan avatar bildja avatar cowchimp avatar danielkag avatar dependabot[bot] avatar dlvhdr avatar etaiso avatar felixb-wix avatar github-sheriff avatar haimwix avatar ilanadre avatar itsh01 avatar katmai7 avatar kgshv avatar manishma avatar meirkoen avatar noacowix avatar ohana54 avatar oleggo avatar orben32 avatar roneng-wix avatar ronenst avatar rudnitskih avatar salickc avatar shirlynwix avatar shirshintel avatar tomenden avatar wix-oss avatar yinonc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

repluggable's Issues

Example application suggestion

Repluggable looks like a really useful library but I'm struggling to get my head around some of the concepts involved. An example application would be really useful to help people get to grips with it and perhaps help to encourage adoption. Perhaps a version of TodoMVC as this is something many people are already familiar with. Thanks for the great work!

Extract runtime APIs from AppHost

AppHost contains many utility/administration functions. These functions can be extracted into APIs. They will be treated as any other API, and stop being special cases.

Below is proposed API names and contents.

AppHostDebugAPI

  • getAllSlotKeys(): AnySlotKey[]
  • getAllFeatures(): FeatureInfo[]

AppHostAdminAPI

  • isFeatureInstalled(name: string): boolean
  • isLazyFeature(name: string): boolean
  • installFeatures(features: AnyFeature[]): void
  • uninstallFeatures(names: string[]): void

Implement monitoring - please review

Capabilities we want to achieve

  • Track and report outcome of operations (success/error)
  • Measure and report duration of operations
  • Label all reported events with operation name, plus the names of related bundle, package, and entry point
  • Application-specific behavior: based on message ID and priority, route events to different infrastructure mechanisms, e.g. sentry / New Relic / etc. This layer will be implemented by application and injected into AppHost.

Proposed API

Examples

  • Call an asynchronous function and report its duration and outcome:

    const saveResult = await shell.log.span(LOG_EVID_PUBLISH, () => {
        return shell.GetAPI(SaveAPI).save()
    })   

    This will wrap the promise returned by SaveAPI.save() and track its duration and outcome. It will report events of 'start' and 'finish' to application-specific logging layer. These events will be automatically labeled with related bundle/package/entry point. The app-specific logging layer will route the events to application-specific infrastructure services.

  • Call a synchronous function and report its duration and outcome:

    const selection = shell.log.span(LOG_EVID_SELECT_COMP, () => {
        return shell.GetAPI(SelectionAPI).select(inputEvent)
    })   

    This will track its duration and outcome of the passed arrow function. It will report events of 'start' and 'finish' to application-specific logging layer. These events will be automatically labeled with related bundle/package/entry point. The app-specific logging layer will route the events to application-specific infrastructure services.

  • Report a one-off event:

    shell.log.warning(LOG_EVID_COMP_NOT_FOUND, { compRef: myCompRef })

    This will report the event and to application-specific logging layer. The event will be automatically labeled with related bundle/package/entry point. The app-specific logging layer will route the event to application-specific infrastructure services.

Full listing

export type LogSeverity = 'debug' | 'info' | 'warning' | 'error';
export type LogSpanFlag = 'open' | 'close';

export interface HostLogger {
    event(
        severity: LogSeverity,
        source: string,
        id: string,
        keyValuePairs?: Object,
        spanFlag?: LogSpanFlag): void;
}

export interface ShellLogger {
    debug(messageId: string, keyValuePairs?: Object): void;
    info(messageId: string, keyValuePairs?: Object): void;
    warning(messageId: string, keyValuePairs?: Object): void;
    error(messageId: string, keyValuePairs?: Object): void;
    // report duration+outcome of an operation:
    span(messageId: string, keyValuePairs: Object, action: () => void): void;
    asyncSpan(messageId: string, keyValuePairs: Object, action: () => Promise<any>): void;
}

New interfaces and properties:

  • HostLogger interface: the application-specific logging layer. Its responsibility is route events to application-specific infrastructure services, e.g. sentry, New Relic, etc. Main application will inject its custom HostLogger object into AppHost by passing it to createAppHost().
  • ShellLogger interface: a fine-grained interface for use by packages. It will be exposed from the Shell object, as a new property named log. The ShellLogger will delegate to HostLogger of the AppHost. So basically ShellLogger is a syntactic sugar over HostLogger.
     

Move base services into their own package

Create a package that includes basic low-level services on top of the AppHost:

  • Mouse
  • Context menu
  • Keyboard shortcuts

Proposed package name: react-app-lego-basic-services (TBD)

How to use repluggable for creating plugins

Hi there
Suppose there is react app. And suppose there are some React feature components, let's say ContactsList, EditContacts, AddContacts. This components must be plugginable. Like how users control and add plugins in WordPress. How this functionality can be implemented with repluggable package?

upgrade to Redux Hooks

With the new Redux Hooks API (https://react-redux.js.org/api/hooks), code uses redux would be simpler. It resolves the pains of using redux by removing needs of mapStateToProps etc.

Does this sounds a good idea? How could replugable to integrate it? Is there a plan?

Implement translations

Design proposal

All translations will be done through a translation function, which will receive a string key and optional placeholder values, and return formatted translation:

type TranslationFunc = (key: string, params?: {[name: string]: any}) => string

Since retrieval of translated texts is widely used, it should be as laconic as possible.

Consumption

There will be three ways to obtain the translation function:

  1. Pure components connected through connectWithShell will get it in props, under the name t:

    const { t } = props
    return <span>{t('LangMenu_Manager_Panel_Title')}</span>
  2. Directly from the Shell object:

    shell.translate('LangMenu_Manager_Panel_Title')
  3. Through ShellContext (useful in a non-connected component):

    <ShellContext.Consumer>{shell => 
        <span>{shell.translate('LangMenu_Manager_Panel_Title')}</span>
    }</ShellContext.Consumer> 

Injection

Assumptions

  • single locale per page
  • locale cannot be changed without reloading the page

Proposal

Translation data will be scoped per Shell (which means per Entry Point)

  • Every entry point will be able to contribute a different translation data
  • This allows slicing of translation data per team

If translation data is of default structure (defined below), default translation function can be used. For data of custom structure, custom translation function must be used.

  • For default structure, an Entry Point has to contribute translation data:
    shell.getAPI(AppHostAPI).contributeTranslations(myTranslationData) 
  • For custom structure, an Entry Point has to setup a custom translation function:
    shell.getAPI(AppHostAPI).useTranslationFunction(myTranslationFunc) 
    In the latter case translation data is not passed: myTranslationFunc must be bound to its data

Default structure of translation data

{
    "first_key": "translation 1",
    "second_key": "translation with named {param-name} parameters",
    "third_key": "translation with indexed {0} parameters {1}"  
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.