Sample application for the Ngx-State-Store module usage features demonstration.
ngx-state-store is based on RxJS, easy to learn and use, light and quick the state management module for the Angular applications starting from the Angular version >= 7.2.0
state-store is the sample Angular application for the ngx-state-store module usage features demonstration.
ngx-state-store and state-store were updated and tested up to the Angular version 17
This documentation is to help you regarding the ngx-state-store module. The presented simplified code examples are to help you to grasp the major idea behind the module.
The benefits of using the ngx-state-store in comparison to other known state management frameworks are: much less written code, better overview over the code, subscription to async actions, simple to refactor by new implementation of an action.
It is expected to have the following knowledge:
export interface AppState { Counter: number; }
import { AppState } from './app-state'; export const AppInitialState: AppState = { Counter: 0 };
// ... import { NgModule } from '@angular/core'; import { AppInitialState } from './services/state-store/app-initial-state'; import { NgxStateStoreModule } from 'ngx-state-store'; // ... @NgModule({ declarations: [ // ... ], imports: [ // ... NgxStateStoreModule.forRoot({ storeName: 'store-demo', log: true, timekeeping: true, initialState: AppInitialState }), // ... ], // ... }) export class AppModule { // ... }
window['ngx-state-store']['store-demo']
.
window['ngx-state-store']['store-demo'].state.log.log = true; window['ngx-state-store']['store-demo'].state.performance.timekeeping = true;
import { Action, StateContext } from 'ngx-state-store'; import { ActionIds } from '../action-ids'; import { AppState } from '../app-state'; export class IncrementCounterAction extends Action { constructor() { super(ActionIds.UpdateCounter); } handleState(stateContext: StateContext<AppState>): void { const newValue = this.getEmptyState(); newValue.Counter = stateContext.getState().Counter + 1; stateContext.patchState(newValue); } }
export enum ActionIds { UpdateCounter = '[Common] update counter' }
[Common] update counter
, will be displayed in
the console log output.
import { Injectable } from '@angular/core'; import { Action } from 'ngx-state-store'; import { IncrementCounterAction } from './actions/increment-counter.action'; @Injectable() export class ActionFactory { incrementCounter(): Action { return new IncrementCounterAction(); } }
store.dispatch(...)
.import { Component } from '@angular/core'; import { AppState } from '../../services/state-store/app-state'; import { ActionFactory } from '../../services/state-store/action-factory'; import { Store } from 'ngx-state-store'; @Component({ selector: 'app-counter-button', templateUrl: './counter-button.component.html', styleUrls: ['./counter-button.component.scss'] }) export class CounterButtonComponent { constructor(private store: Store<AppState>, private factory: ActionFactory) { } incrementCounter() { this.store.dispatch(this.factory.incrementCounter()); } }
store.select(...)
.import { Component, OnInit } from '@angular/core'; import { Store } from 'ngx-state-store'; import { AppState } from '../../services/state-store/app-state'; import { Observable } from 'rxjs'; @Component({ selector: 'app-counter', templateUrl: './counter.component.html', styleUrls: ['./counter.component.scss'] }) export class CounterComponent implements OnInit { counter$: Observable<number>; constructor(private store: Store<AppState>) { } ngOnInit(): void { this.counter$ = this.store.select('Counter'); } }
select(...)
method returns an Observable. It is a bad design / architecture to put the state
management inside of the components as the components must be / are expected to be reusable. The
better approach is to put the state management inside of so called container-components.
Container-components are the components, that contain all of the reusable components.
The design and architecture are out of the scope of this documentation. The presented
examples are simplified for the sake of the clarity.
The more complex use case includes a back-end call.
import { Inventory } from '../../models/inventory'; export interface AppState { ShowLoadingIndicator: string[]; Counter: number; Inventories: Inventory[]; // iso Date string LastDownloadAt: string; }
export class Inventory { id: number; version: string; name: string; }
import { AppState } from './app-state'; export const AppInitialState: AppState = { ShowLoadingIndicator: [], Counter: 0, Inventories: null, LastDownloadAt: '' };
import { Action, StateContext } from 'ngx-state-store'; import { ActionIds } from '../action-ids'; import { AppState } from '../app-state'; export class ShowLoadingIndicatorAction extends Action { constructor(private identifier: string) { super(ActionIds.ShowLoadingIndicator + ': ' + identifier); } handleState(stateContext: StateContext<AppState>): void { const newState = this.getEmptyState(); newState.ShowLoadingIndicator = stateContext.getState().ShowLoadingIndicator.slice(); newState.ShowLoadingIndicator.push(this.identifier); stateContext.patchState(newState); } }
import { Action, StateContext } from 'ngx-state-store'; import { ActionIds } from '../action-ids'; import { AppState } from '../app-state'; export class HideLoadingIndicatorAction extends Action { constructor(private identifier: string) { super(ActionIds.HideLoadingIndicator + ': ' + identifier); } handleState(stateContext: StateContext<AppState>): void { if (stateContext.getState().ShowLoadingIndicator == null) { return; } const index = stateContext.getState() .ShowLoadingIndicator.indexOf(this.identifier); if (index < 0) { return; } const newState: AppState = this.getEmptyState(); newState.ShowLoadingIndicator = stateContext.getState().ShowLoadingIndicator.slice(); newState.ShowLoadingIndicator.splice(index, 1); stateContext.patchState(newState); } }
import { Action, StateContext } from 'ngx-state-store'; import { ActionIds } from '../action-ids'; import { AppState } from '../app-state'; import { InventoryConnector } from '../../connectors/inventory.connector'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; export class LoadInventoriesAction extends Action { constructor(private inventoryConnector: InventoryConnector) { super(ActionIds.LoadInventories); } handleState(stateContext: StateContext<AppState>): Observable<any> { return this.inventoryConnector.loadInventory() .pipe( tap(inventories => { const newState: AppState = this.getEmptyState(); newState.Inventories = inventories; newState.LastDownloadAt = (new Date()).toISOString(); stateContext.patchState(newState); }) ); } }
import { Injectable } from '@angular/core'; import { Action } from 'ngx-state-store'; import { ShowLoadingIndicatorAction } from './actions/show-loading-indicator.action'; import { IncrementCounterAction } from './actions/increment-counter.action'; import { HideLoadingIndicatorAction } from './actions/hide-loading-indicator.action'; import { LoadInventoriesAction } from './actions/load-inventories.action'; import { InventoryConnector } from '../connectors/inventory.connector'; export enum LoadIndicator { DEFAULT = 'DEFAULT', LOAD_INVENTORIES = 'LOAD_INVENTORIES' } @Injectable() export class ActionFactory { constructor(private inventoryConnector: InventoryConnector) { } incrementCounter(): Action { return new IncrementCounterAction(); } showLoadIndicator(identifier: string = LoadIndicator.DEFAULT): Action { return new ShowLoadingIndicatorAction(identifier); } hideLoadIndicator(identifier: string = LoadIndicator.DEFAULT): Action { return new HideLoadingIndicatorAction(identifier); } loadInventories(): Action { return new LoadInventoriesAction(this.inventoryConnector); } }
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Inventory } from '../../models/inventory'; import { delay } from 'rxjs/operators'; @Injectable() export class InventoryConnector { constructor(private http: HttpClient) { } loadInventory(): Observable<Inventory[]> { // delay(2000) to imitate the network throttling return this.http.get<Inventory[]>('assets/mock-data/inventories.json') .pipe(delay(2000)); } }
store.dispatch(...)
import { Component, OnInit } from '@angular/core'; import { Store } from 'ngx-state-store'; import { AppState } from '../../services/state-store/app-state'; import { ActionFactory, LoadIndicator } from '../../services/state-store/action-factory'; import { catchError, mergeMap, skip } from 'rxjs/operators'; import { of } from 'rxjs'; import { Inventory } from '../../models/inventory'; export interface Changes { addedEntries: Inventory[]; removedEntries: Inventory[]; } @Component({ selector: 'app-inventories-button', templateUrl: './inventories-button.component.html', styleUrls: ['./inventories-button.component.scss'] }) export class InventoriesButtonComponent implements OnInit { lastDownloadAt: string; changes: Changes = {addedEntries: [], removedEntries: []} as Changes; private inventories: Inventory[] = []; constructor(private store: Store<AppState>, private factory: ActionFactory) { } ngOnInit(): void { this.store.select('Inventories', (oldInventories: Inventory[], newInventories: Inventory[]) => { if (oldInventories === newInventories || oldInventories && newInventories && !this.calcDiff(oldInventories, newInventories).length && !this.calcDiff(newInventories, oldInventories).length) { return true; } return false; }).pipe(skip(1)) .subscribe((newInventories) => { this.changes.addedEntries = this.calcDiff(this.inventories, newInventories); this.changes.removedEntries = this.calcDiff(newInventories, this.inventories); this.inventories = newInventories; console.log('the log is present only if there are some changes'); }); } private calcDiff(source: Inventory[], target: Inventory[]): Inventory[] { return (target || []).filter(t => !(source || []).find(s => s.id === t.id)); } loadInventory() { this.changes.addedEntries = []; this.changes.removedEntries = []; this.store.dispatch( this.factory.showLoadIndicator(LoadIndicator.LOAD_INVENTORIES)) .pipe( mergeMap(() => this.store.dispatch(this.factory.loadInventories())), catchError(error => { console.log(error); return of(error); }) ).subscribe((state: AppState) => { this.lastDownloadAt = state.LastDownloadAt; this.store.dispatch(this.factory.hideLoadIndicator(LoadIndicator.LOAD_INVENTORIES)); } ); } inventoriesToString(inventories: Inventory[]): string { return inventories.map(e => e.id).toString(); } }
import { Component, OnInit } from '@angular/core'; import { Store } from 'ngx-state-store'; import { AppState } from '../../services/state-store/app-state'; import { Inventory } from '../../models/inventory'; import { Observable, of } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { LoadIndicator } from '../../services/state-store/action-factory'; @Component({ selector: 'app-inventories', templateUrl: './inventories.component.html', styleUrls: ['./inventories.component.scss'] }) export class InventoriesComponent implements OnInit { inventories: Inventory[]; loading$: Observable<boolean>; constructor(private store: Store<AppState>) { } ngOnInit(): void { this.loading$ = this.store.select('ShowLoadingIndicator').pipe( map(indicators => indicators.filter(i => i === LoadIndicator.LOAD_INVENTORIES).length > 0) ); this.store.select('Inventories').subscribe(inventories => { this.inventories = inventories; }); } }
store.select(...)
return a frozen (read only) state
objects that were frozen by the StateHelper.deepFreeze(any)
. Use
StateHelper.cloneObject(any, cloneFunctions = true)
to get a clone of the frozen object
including the functions (default) if it is needed. Cloning the functions is in experimental stage.
Usually the state objects do not have any functions, and it is not recommended they have any.
Keep in mind that all objects passed to the state
store will be frozen. The freeze process is based on JavaScript Object.freeze()
select(string, ObjectComparator?): Observable<any>
- select any state of the state store by the first parameter - the state key, the second parameter
ObjectComparator
is optional. The ObjectComparator can be used to implement a more
advanced comparison of potential changes. It is also a convenient way to only subscribe to
some property/properties changes of any state object.selectOrDefault(string, any, ObjectComparator?): Observable<any>
- select any state of the state store by the first parameter - the state key or return the default
value, the second parameter, if the state is undefined or null. The third parameter
ObjectComparator
is optional.selectSubProperty(string, string, ObjectComparator?): Observable<any>
- select any state sub-property of the state store by the first parameter - the state key. The second
parameter sub-property path is a string representation of the property path. For example: if
any state identified by 'key1' has a property object with the name 'prop1', and this object has an
array of other objects saved in property 'prop2', and the property 'prop3' of the second
element (index 1) of the object from the array is needed, then the sub-property
path would be: 'prop1/prop2/1/prop3'. The third parameter ObjectComparator
is
optional.select(string, string, any, ObjectComparator?): Observable<any>
- select any state sub-property of the state store by the first parameter - the state key or return
the default value for the sub-property, the third parameter, if the sub-property is undefined
or null. The second parameter is the sub-property path, explanation above. The fourth parameter
ObjectComparator
is optional.selectOnce(string): Observable<any>
- the same as select
but the Observable terminates after forward one value.selectOnceOrDefault(string, any): Observable<any>
- the same as selectOrDefault
but the Observable terminates after forward one value.selectOnceSubProperty(string, string): Observable<any>
- the same as selectSubProperty
but the Observable terminates after forward one value.selectOnceSubPropertyOrDefault(string, string, any): Observable<any>
- the same as selectSubPropertyOrDefault
but the Observable terminates after forward one value.dispatch(action: Action): Observable<state: S>
- dispatch the Action that changes some state, the dispatch function always returns an
Observable of your newest state. Tip: You could join many dispatch
calls in a pipe and have access to the last state change from the previous call. In this case,
the preferable way to do any changes of the state is by returning an Observable. The Observable
itself returning value does not have any meaning. For the return value inside of the Observable
you could use return of(null);
from rxjs, or operator function tap((x: T) =>
void)
.
see the examples: inventories-button.component.ts and load-inventories.action.ts
in the example load-inventories.action.ts the access to the <state: S> is in the
subscription.
this.store.dispatch(<some action>) .pipe( mergeMap((state: S) => { // ... read and use data from the state return this.store.dispatch(<another action>); }), mergeMap((state: S) => { // ... read and use data from the state return this.store.dispatch(<another action 2>); }) ).subscribe((state: S) => { // ... read and use data from the state });
store.select(string)
in the subscription / pipe to
store.dispatch(<some action>)
.
store.dispatch(<some action>)
returns an Observable that emits only one value,
that is newest state, and terminates, whereas the store.select(string)
returns an
Observable that emits the value every time the state was changed. As the result the subscription
stays further active.
store.selectOnce(string)
returns stale state if it is
placed in the subscription / pipe to store.dispatch(<some action>)
. This applies, when
the dispatching action changes the state.
store.dispatch(<some action>)
and the state of the StateContext in the action.handleState(stateContext)
are the
newest in the subscription / pipe to store.dispatch(<some action>)
.
store.dispatch(<some action>)
rulesdescribe('dispatch, pipe and selectOnce', () => { let store: Store<any>; beforeEach(() => { TestBed.configureTestingModule({ providers: [Store, { provide: STATE_CONFIG, useValue: {initialState: {Counter: 0}} // initial Counter = 0 }] }); store = TestBed.inject(Store); }); it('selecting stale state by subscribe to dispatch', (done) => { of(null) .pipe( mergeMap(() => store.dispatch({ handleState(stateContext: StateContext<any>): Observable<void> { expect(stateContext.getState().Counter).toBe(0); // initial - Counter = 0 return of(null) .pipe( tap(() => { stateContext.patchState({Counter: 1}); // patch state - Counter = 1 }) ); } } as Action)), mergeMap((newState) => store.dispatch({ handleState(stateContext: StateContext<any>): Observable<void> { // the returned state from dispatch is always new state - Counter = 1 expect(newState.Counter).toBe(1); // new state - Counter = 1 // the state of the stateContext is also new state - Counter = 1 expect(stateContext.getState().Counter).toBe(1); // new state - Counter = 1 return of(null) .pipe( tap(() => { stateContext.patchState({Counter: 2}); // patch state - Counter = 2 }) ); } } as Action)) ) .subscribe(newState => { // the returned state from dispatch is always new state - Counter = 2 expect(newState.Counter).toBe(2); // new state - Counter = 2 // the returned state from selectOnce is stale state - Counter = 1 store.selectOnce('Counter').subscribe((staleCounterValue) => { expect(staleCounterValue).toBe(1); // stale state - Counter = 1 done(); }); }); }); });
export type ObjectComparator = (oldObject: any, newObject: any) => boolean;
- is a
function. The ObjectComparator can be used by
Store.select(string, ObjectComparator?): Observable<any>
as second parameter.
By the ObjectComparator you may implement more advanced comparison of the potential changes.
The method must return true
if there are no changes you are interested in. For
example the property/properties of some state object was/were not changed.
this.store.select('<some state>', (oldObjectState: any, newObjectState: any) => { // here you can compare if the value of some property was changed if (oldObjectState === newObjectState || oldObjectState && newObjectState && oldObjectState['someProperty'] === newObjectState['someProperty']) { // nothing changed return true; } // someProperty of the state object was changed return false; }).subscribe((state: S) => { // ... do somethig if the newObjectState partly changed });
Besides the simply structured JSON objects, also the objects of the types: Date, Map and Set may be used in the state. The StateHelper can make them immutable and clone them accordingly. Also the objects with any kind of cyclic dependencies of its properties may be used in the state, for example composite or hierarchical properties or lists with the parent child relations where the leaves reference each other in different ways. The StateHelper can make them immutable and clone them respectively, and it keeps the relations.
static deepFreeze<T>(o: T): T
- freezes the object, the object is read only after the call. Also the objects/properties of
the types: Sat, Map and Date are immutable. The freeze process is based on JavaScript
Object.freeze()
.static cloneObject<T>(o: T, cloneFunctions = true): T
- creates a clone of the object including functions (default), the method is useful if the
object was frozen by the deepFreeze
. The method handles also objects with any
kind of cyclic dependencies of its properties. Cloning the functions is in experimental stage.
Usually the state objects do not have any functions, and it is not recommended they have any.getState(): S
- returns the whole current state.patchState(val: Partial<S>)
- patch the existing state with the provided value.setState(state: S)
- reset the whole state to a new value.abstract handleState(stateContext: StateContext<any>): Observable<any> | void
- it must be implemented by the user.clone<T>(o: T, cloneFunctions = true): T
- clone the object including functions (default), the same as StateHelper.cloneObject(o,
cloneFunctions = true)
. Cloning the functions is in experimental stage. Usually the
state objects do not have any functions, and it is not recommended they have any.The source code can be downloaded from the GitHub https://github.com/it-and-services/state-store.
ngx-state-store : NPM module online https://www.npmjs.com/package/ngx-state-store.
state-store : The Simple example and More complex use case presented here in the documentation are running as the online App https://it-and-services.github.io/state-store/app.html.
The history section consists of two parts. The first part is regarding the ngx-state-store changes. The second part is regarding the documentation changes.
----------------------------------------------------------------------------------------- Version 1.1.1 - 03 Mar 2023 ----------------------------------------------------------------------------------------- - performance optimization - clean up deprecations ----------------------------------------------------------------------------------------- Version 1.1.0 - 03 Feb 2023 ----------------------------------------------------------------------------------------- - clean up RxJs deprecations that will be removed in v8, the clean up was done with backward compatibility - new comfort methods presented: - selectOrDefault(...) returns default value if the state is undefined or null - selectSubProperty(...) returns state object property by path - selectSubPropertyOrDefault(...) returns state object property by path or default value if the property is undefined or null - duplicate methods of the listed above for single select - selectOnce...(...) ----------------------------------------------------------------------------------------- Version 1.0.18 - 28 Oct 2021 ----------------------------------------------------------------------------------------- - the Angular update remark was moved out of the release into the documentation to avoid empty releases just because of the Angular update ----------------------------------------------------------------------------------------- Version 1.0.17 - 21 May 2021 ----------------------------------------------------------------------------------------- - updated and tested up to the Angular 12 ----------------------------------------------------------------------------------------- Version 1.0.16 - 29 Apr 2021 ----------------------------------------------------------------------------------------- - Date, Map and Set objects may be used in the state - StateHelper.cloneObject() was extended to clone Date, Map and Set - StateHelper.deepFreeze() was extended to make Date, Map and Set immutable ----------------------------------------------------------------------------------------- Version 1.0.15 - 14 Apr 2021 ----------------------------------------------------------------------------------------- - cyclic dependencies resolution by the StateHelper.cloneObject() - prevent to clone the window by the StateHelper.cloneObject() - prevent to freeze the window by the StateHelper.deepFreeze() - check return value of the Action.handleState() by rxjs isObservable() ----------------------------------------------------------------------------------------- Version 1.0.14 - 28 Mar 2021 ----------------------------------------------------------------------------------------- - externalize the documentation ----------------------------------------------------------------------------------------- Version 1.0.13 - 20 Nov 2020 ----------------------------------------------------------------------------------------- - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.12 - 20 Nov 2020 ----------------------------------------------------------------------------------------- - updated to Angular 11 - extend performance plugin by the 'limit' parameter, default value is 1000 - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.11 - 13 Jul 2020 ----------------------------------------------------------------------------------------- - improve documentation - update example application ----------------------------------------------------------------------------------------- Version 1.0.10 - 26 Jun 2020 ----------------------------------------------------------------------------------------- - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.9 - 28 Apr 2020 ----------------------------------------------------------------------------------------- - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.8 - 25 Mar 2020 ----------------------------------------------------------------------------------------- - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.7 - 10 Feb 2020 ----------------------------------------------------------------------------------------- - improve documentation ----------------------------------------------------------------------------------------- Version 1.0.6 - 30 Jan 2020 ----------------------------------------------------------------------------------------- - hide second parameter in the StateHelper.cloneObject(any) ----------------------------------------------------------------------------------------- Version 1.0.5 - 28 Jan 2020 ----------------------------------------------------------------------------------------- - improve documentation - increase test coverage ----------------------------------------------------------------------------------------- Version 1.0.4 - 14 Jan 2020 ----------------------------------------------------------------------------------------- - add clone Date into the StateHelper.cloneObject() ----------------------------------------------------------------------------------------- Version 1.0.3 - 8 Jan 2020 ----------------------------------------------------------------------------------------- - add keywords into the package.json ----------------------------------------------------------------------------------------- Version 1.0.2 - 7 Jan 2020 ----------------------------------------------------------------------------------------- - improve documentation - the plugins are always enabled ----------------------------------------------------------------------------------------- Version 1.0.1 - 6 Jan 2020 ----------------------------------------------------------------------------------------- - improve documentation - improve build process - increase test coverage ----------------------------------------------------------------------------------------- Version 1.0.0 - 5 Jan 2020 ----------------------------------------------------------------------------------------- - first release - supported applications with Angular version >= 7.2.0 ----------------------------------------------------------------------------------------- Version 0.0.1 - 4 Jan 2020 ----------------------------------------------------------------------------------------- - first beta release
----------------------------------------------------------------------------------------- Version 1.0.10 - 06 Jan 2024 ----------------------------------------------------------------------------------------- - Remark about Angular update/test upto the version 17 ----------------------------------------------------------------------------------------- Version 1.0.9 - 02 Jul 2023 ----------------------------------------------------------------------------------------- - Remark about Angular update/test upto the version 16 ----------------------------------------------------------------------------------------- Version 1.0.8 - 03 Mar 2023 ----------------------------------------------------------------------------------------- - New ngx-state-store 1.1.1 release ----------------------------------------------------------------------------------------- Version 1.0.7 - 03 Feb 2023 ----------------------------------------------------------------------------------------- - New ngx-state-store 1.1.0 release ----------------------------------------------------------------------------------------- Version 1.0.6 - 06 Jun 2023 ----------------------------------------------------------------------------------------- - Remark about Angular update upto the version 15 ----------------------------------------------------------------------------------------- Version 1.0.5 - 28 Aug 2022 ----------------------------------------------------------------------------------------- - Remark about "cloneFunctions = true"; Cloning the functions is in experimental stage. Usually the state objects do not have any functions, and it is not recommended they have any. ----------------------------------------------------------------------------------------- Version 1.0.4 - 20 Jun 2022 ----------------------------------------------------------------------------------------- - Remark about Angular update upto the version 14 ----------------------------------------------------------------------------------------- Version 1.0.3 - 8 Nov 2021 ----------------------------------------------------------------------------------------- - Remark about Angular update upto the version 13 ----------------------------------------------------------------------------------------- Version 1.0.2 - 21 May 2021 ----------------------------------------------------------------------------------------- - StateHelper functional description see: 'API overview/StateHelper' section ----------------------------------------------------------------------------------------- Version 1.0.1 - 29 Apr 2021 ----------------------------------------------------------------------------------------- - introduction of dispatch rules see: 'API overview/Store/dispatch rules' section ----------------------------------------------------------------------------------------- Version 1.0.0 - 28 Mar 2021 ----------------------------------------------------------------------------------------- - initial HTML based documentation
JavaScript is a registered trademark of Oracle Corporation.
TypeScript is a registered trademark of Lotus Development Corporation.
IntelliJ is a registered trademarks of JetBrains s.r.o.
WebStorm is a registered trademarks of JetBrains s.r.o.
Microsoft is a registered trademark of Microsoft Corporation.
Visual Studio Code is a registered trademark of Microsoft Corporation.
All other product, company, and service names as well as company logos mentioned are the property of their respective owners and are mentioned for identification purposes only.
Code released under the MIT License.
For more information about copyright and license check choosealicense.com.