khamsa
v0.11.0
Published
Build your React.js apps by modules and dependency injecting.
Downloads
5
Readme
Khamsa
Build your React.js apps by modules and dependency injecting.
Introduction
Khamsa is a framework for building robust, clean and scalable React.js applications. It based on TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming).
Motivation
React.js has greatly helped developers build fast and responsive web applications, while its simplicity has also allowed it to accumulate a large number of users in a short period of time, and some large websites have started to be built entirely using React.js. All of this speaks volumes about the success of React.js. However, there are a number of architectural problems with building large web applications using React.js that add up to additional and increasingly large expenses for maintaining and iterating on the project, and Khamsa was created to solve these problems.
Inspired by Angular and Nest.js and based on React.js and React Router, Khamsa provides an out-of-the-box experience to help developers create highly available, highly maintainable, stable, and low-coupling React applications.
Installation & Setup
Requirements
- (Required) Use TypeScript to write project
- (Required) React v16.8.0 or later
- (Required) React Router DOM v6.2.0 or later
- (Required) Webpack v5 or later
- (Recommended) Node.js v10.10.0 or later
Create a React.js + TypeScript Project With CRA
You can use the official-recommended CLI tools CRA (create-react-app) to generate the standard React.js App with TypeScript:
npm i create-react-app -g
mkdir example-project
cd example-project && create-react-app --template cra-template-typescript
Install Khamsa as a Dependency
In the root directory of your React.js app, run following command:
npm i khamsa -S
Configure TypeScript
In your tsconfig.json
file in the project root directory, add following options into it:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
}
Configure Babel
Install Babel plugins:
npm i babel-plugin-transform-typescript-metadata -D
npm i @babel/plugin-proposal-decorators -D
npm i @babel/plugin-proposal-class-properties -D
In your .babelrc
or .babelrc.json
or other types of configuration file for Babel, write the code as below:
{
"plugins": [
"babel-plugin-transform-typescript-metadata",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true,
},
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true,
},
],
]
}
For
CRA
users, please checkout the example inconfig-overrides.js
.
Overview
Providers
Providers are the most important and fundamental concept in Khamsa. Almost any class can be treated as a provider by Khamsa: services, components, tool libraries, etc. Khamsa makes it possible to establish various relationships between different provider objects by injecting dependencies.
As you can see in the image above, each provider can depend on another provider by passing parameters with the provider class as a type annotation in the constructor. With the Khamsa runtime, these type annotation-based provider parameters will be instantiated and made available when the web application starts.
Components
Components are also a type of provider. Like normal providers, any provider (including components) can be injected into a component as a dependency, and similarly, a component can be injected into any provider as a dependency.
A view is a special component that is considered the carrier of a page in Khamsa. It can define routing paths, lazy loading fallbacks, and other options that are not supported by the component.
Modules
A module is a class annotated with a @Module()
decorator. The @Module()
decorator provides metadata that Khamsa makes use of to organize the application structure.
When a Khamsa instance is to be initialized, one and only one module, called the root module, must be provided as the entry module for the application built by Khamsa.
Usages
Create a Provider
The following example shows how to create a provider:
// demo.service.ts
import { Injectable } from 'khamsa';
@Injectable()
export class DemoService {}
Is it unimaginably easy? Yes, that's all the things you should do to create a Provider.
If you want to use other providers as dependencies to be injected, you should declare them in as formal parameters:
// demo.service.ts
import { Injectable } from 'khamsa';
import { FooService } from '../foo/foo.service';
@Injectable()
export class DemoService {
public constructor(
private readonly fooService: FooService,
) {}
}
Then you can use FooService
's instance in DemoService
by calling this.fooService
signature.
Create a Component
Before creating a component class, a JSX file (TSX for TypeScript) needs to be prepared to describe the structure of the component and the component interaction logic, then decorate a class with the @Component decorator and bring in the previous JSX/TSX file:
// foo.component.ts
import Foo from './Foo';
@Component({
component: Foo,
})
export class FooComponent {}
Dependency Injecting
Injecting dependency could be a little different from providers. You should specify the declarations
parameter for @Component
decorator. It is an array that includes the classes which the component class depends on:
// foo.component.ts
@Component({
component: Foo,
declarations: [
FooService,
BarService,
BarComponent,
],
})
export class FooComponent {}
in the JSX/TSX file, you can deconstruct a property named declarations
and use the get
methods in it to use your injected providers:
// Foo.tsx
import {
FC,
PropsWithChildren,
} from 'react';
import { InjectedComponentProps } from 'khamsa';
import { BarComponent } from '../bar/bar.component';
import { FooService } from '../foo/foo.service';
import { BarService } from '../bar/bar.service';
export default Foo: FC<PropsWithChildren<InjectedComponentProps>> = ({ declarations }) => {
const Bar = declarations.get<FC<PropsWithChildren>>(BarComponent);
const fooService = declarations.get<FooService>(FooService);
const barService = declarations.get<BarService>(BarService);
}
Or use can use top-level API getContainer
to get the providers:
// Foo.tsx
import { FC } from 'react';
import { getContainer } from 'khamsa';
import { BarComponent } from '../bar/bar.component';
import { FooService } from '../foo/foo.service';
import { BarService } from '../bar/bar.service';
export default Foo: FC = () => {
// pass the functional component to `getContainer` method
const container = getContainer(Foo);
const Bar = container.get<FC<PropsWithChildren>>(BarComponent);
const fooService = container.get<FooService>(FooService);
const barService = container.get<BarService>(BarService);
}
In the next major distribution, getting dependencies from
props.declarations
will not be supported any more.
forwardContainer
Khamsa provides a top-level API called forwardContainer
to help you obtain references to containers when using React HOC:
// Foo.tsx
import {
FC,
memo,
} from 'react';
import { forwardContainer } from 'khamsa';
import { BarComponent } from '../bar/bar.component';
import { FooService } from '../foo/foo.service';
import { BarService } from '../bar/bar.service';
const Foo: FC = forwardContainer(({ props, container }) => {
const Bar = container.get<FC<PropsWithChildren>>(BarComponent);
const fooService = container.get<FooService>(FooService);
const barService = container.get<BarService>(BarService);
});
export default memo(Foo);
Lazy Load
Khamsa supports lazy load based on React's .lazy
and Suspense
:
// foo.component.ts
import { lazy } from 'react';
@Component({
component: lazy(() => import('./Foo')),
})
export class FooComponent {}
Error Boundaries
You can define a custom boundary component for every components in Khamsa:
// FooBoundary.tsx
import {
FC,
useEffect,
} from 'react';
const FooBoundary: FC = () => {
useEffect(() => {
throw new Error('Error thrown');
}, []);
return (<>Boundary test</>);
};
export default FooBoundary;
// foo-boundary.component.tsx
import { Component } from 'khamsa';
import {
ErrorBoundary,
ErrorBoundaryPropsWithFallback,
} from 'react-error-boundary';
import FooBoundary from './FooBoundary';
import { PropsWithChildren } from 'react';
@Component({
component: FooBoundary,
boundaryComponent: (props: PropsWithChildren<ErrorBoundaryPropsWithFallback>) => {
return (
<ErrorBoundary fallback={<pre>ERROR CAUGHT</pre>}>
{props.children}
</ErrorBoundary>
);
},
})
export class FooBoundaryComponent {}
The definition of @Component
's parameters are like below:
component?: React.FC
- the React component declarationfactory?: (forwardRef: FactoryForwardRef) => React.FC<P> | React.ExoticComponent<P>
- the component factory, it passes aforwardRef
method to inject dependencies into component witch would be returned by thefactory
function. Whencomponent
andfactory
are all set,factory
will take the higher prioritydeclarations?: Array<Type>
- the provider classes depended by current componentelementProps?: any
- props for current view's React componentsuspenseFallback?: boolean | null | React.ReactChild | React.ReactFragment | React.ReactPortal
- the value offallback
property forReact.Suspense
Create a Module
Module is also a normal class with a @Module
decorator:
// demo.module.ts
import { Module } from 'khamsa';
@Module()
export class DemoModule {}
Export & Import
Here is an example of using imports and exports to share providers between modules:
.
└── src/
└── modules/
├── foo/
│ ├── foo.module.ts
│ └── foo.service.ts
└── bar/
├── bar.module.ts
└── bar.service.ts
foo.service.ts
is a provider for the FooModule
, which is declared and exported by the FooModule
:
// foo.service.ts
@Injectable()
export class FooService {
public sayFooHello() {
console.log('Greets from FooService!');
}
}
// foo.module.ts
@Module({
providers: [
FooService,
],
exports: [
FooService,
],
})
export class FooModule {}
Now, the BarService
in the BarModule
wants to have access to the sayFooHello
method in the FooService
, so the FooModule
can be brought in via the imports option in bar.module.ts
:
// bar.module.ts
@Module({
imports: [
FooModule,
],
providers: [
BarService,
],
})
export class BarModule {}
Next, the BarService
in bar.service.ts
can pass the FooService
as a type annotation with one parameter into the constructor:
// bar.service.ts
@Injectable()
export class BarService {
public constructor(
private readonly fooService: FooService,
) {}
public sayBarHello() {
console.log('Greets from BarService!');
this.fooService.sayFooHello();
}
}
Code Splitting When Importing Modules
With Webpack 5's code splitting feature, Khamsa will also split your code when you use dynamic imports to import modules:
// bar.module.ts
@Module({
imports: [
import('../foo/foo.module').then(({ FooModule }) => FooModule),
],
providers: [
BarService,
],
})
export class BarModule {}
Configuring Routes
Following the previous example, now the project looks like this:
.
└── src/
└── modules/
├── foo/
│ ├── foo.module.ts
│ ├── foo.service.ts
│ ├── foo.component.ts
│ └── Foo.tsx
└── bar/
├── bar.module.ts
└── bar.service.ts
You should add routes
option to @Module
:
// foo.module.ts
@Module({
components: [
FooComponent,
],
providers: [
FooService,
],
exports: [
FooService,
],
routes: [
{
path: 'foo',
useComponentClass: FooComponent,
},
],
})
export class FooModule {}
Khamsa will parse the route config and get a path of /foo
who renders Foo.tsx
.
You can also use a module class to configure the routes by passing useModuleClass
option. Now the project looks like this:
.
└── src/
└── modules/
├── foo/
│ ├── foo.module.ts
│ ├── foo.service.ts
│ ├── foo.component.ts
│ └── Foo.tsx
├── bar/
│ ├── bar.module.ts
│ └── bar.service.ts
└── baz/
├── baz.module.ts
├── baz.service.ts
├── baz-child.component.ts
├── BazChild.tsx
├── baz.component.ts
└── Baz.tsx
The baz.module.ts
's definition looks like this:
// baz.module.ts
@Module({
components: [
BazComponent,
BazChildComponent,
],
providers: [
BazService,
],
exports: [
BazComponent,
BazChildComponent,
BazService,
],
routes: [
{
path: 'baz',
useComponentClass: BazComponent,
children: [
{
path: 'child',
useComponentClass: BazChildComponent,
},
],
},
],
})
export class BazModule {}
and the foo.module.ts
's content:
// foo.module.ts
@Module({
imports: [
BazModule,
],
components: [
FooComponent,
],
providers: [
FooService,
],
exports: [
FooService,
],
routes: [
{
path: 'foo',
useComponentClass: FooComponent,
children: [
{
useModuleClass: BazModule,
},
],
},
],
})
export class FooModule {}
Khamsa will parse it into /foo
, /foo/baz
and /foo/baz/child
routes.
The @Module()
decorator takes a single object as parameter whose properties describe the module:
imports: Array<Module>
- the list of imported modules that export the providers which are required in this moduleproviders: Array<Provider>
- the list of providers that the module hosts, which could probably be used by other modulescomponents: Array<Component>
- the list of components provided by current moduleexports: Array<Provider>
- the subset ofproviders
that are provided by this module and should be available in other modules which import this moduleroutes: Array<RouteOptionItem>
- the list of routes provided by current module
The definition of RouteOptionItem
is like below:
path: string
- (required) defines the route that the view matches, must be an absolute pathuseComponentClass?: Type
- the provider who carries the component classuseModuleClass?: Type
- the provider who carries a module class with routes configcaseSensitive?: boolean
- defines the route matcher should use case-sensitive mode or notindex?: number
- specify if current view is an indexed route
Use Root Module to Create an App
See this code to get detailed information of how to create a React.js App by Khamsa.
Participate in Project Development
Getting involved in the development of Khamsa is welcomed. But before that, please read the Code of Conduct of Khamsa. You can also read this doc to get more information about contribute your code into this repository.
Before starting working on the project, please upgrade your Node.js version to v14.15.0 or later.
Sponsorship
We accept sponsorship and are committed to spending 100% of all sponsorship money on maintaining Khamsa, including but not limited to purchasing and maintaining the Khamsa documentation domain, servers, and paying stipends to some of our core contributors.
Before initiating a sponsorship, please send an email to [email protected] or [email protected] with your name, nationality, credit card (VISA or MasterCard) number, what problem Khamsa has helped you solve (optional), and a thank-you message (optional), etc. After review and approval, we will reply with an email with a payment method that you can complete the sponsorship via this email.
Thank you so much for your support of the Khamsa project and its developers!