{"id":31999339,"url":"https://github.com/MaxxtonGroup/ngx-mxstore","last_synced_at":"2025-10-15T14:08:22.057Z","repository":{"id":63869666,"uuid":"566830075","full_name":"MaxxtonGroup/ngx-mxstore","owner":"MaxxtonGroup","description":null,"archived":false,"fork":false,"pushed_at":"2025-08-21T14:30:55.000Z","size":1377,"stargazers_count":2,"open_issues_count":1,"forks_count":2,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-08-21T17:04:23.228Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MaxxtonGroup.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-11-16T14:09:01.000Z","updated_at":"2025-08-21T14:31:00.000Z","dependencies_parsed_at":"2025-08-21T16:28:44.341Z","dependency_job_id":"72598037-ee77-45f8-a8d0-5a7115b175a8","html_url":"https://github.com/MaxxtonGroup/ngx-mxstore","commit_stats":{"total_commits":13,"total_committers":2,"mean_commits":6.5,"dds":"0.23076923076923073","last_synced_commit":"5aa97c49e08a70d1857f9574af583544efba9cf6"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MaxxtonGroup/ngx-mxstore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxxtonGroup%2Fngx-mxstore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxxtonGroup%2Fngx-mxstore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxxtonGroup%2Fngx-mxstore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxxtonGroup%2Fngx-mxstore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MaxxtonGroup","download_url":"https://codeload.github.com/MaxxtonGroup/ngx-mxstore/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxxtonGroup%2Fngx-mxstore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279085144,"owners_count":26100014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-15T02:00:07.814Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-15T14:01:36.208Z","updated_at":"2025-10-15T14:08:22.051Z","avatar_url":"https://github.com/MaxxtonGroup.png","language":"TypeScript","funding_links":[],"categories":["State Management"],"sub_categories":["Other State Libraries"],"readme":"# ngx-mxstore\n\n## Installation\n\n```\nnpm install -s ngx-mxstore\n```\n\n## Purpose of the ngx-mxstore\n- keep state out of components\n- it helps to move business logic inside of pure methods\n- it helps to create testable code\n\n## Basic Example\nA component can be decorated with the StoreAware decorator. \nThis will connect the component with the store and gives it superpowers!\n\n```ts\n@Component({\n  ...\n})\n@StoreAware()\nexport class CalculatorComponent {\n  state: CalculatorState;\n  \n  get currentValue() {\n      return CalculatorSelectors.currentValue( this.state );\n  }\n  \n  constructor(private calculator: CalculatorStoreService) {\n  }\n\n  add() {\n    CalculatorActions.add( { amount: 4 } );\n  }\n}\n```\n\nThe StoreAware decorator will find the CalculatorStoreService injected in the constructor\nand provide that state inside the component. This state variable will always reflect the\nmost recent version of the state.\n\nVia actions we can mutate the state. With selectors we can fetch data from the state.\n\nThe idea of ngx-mxstore is that you can have many different stores in your application. They will all be stored in a single global state.\nThe reason of this approach is that you can subdivide your application in different parts and keep the state of each part in a separate store.\n\nSometimes there is a need to have multiple instances of the same store, this is also possible and will be explained in [the advanced-usage chapter](#advanced-usage).\n\nOk there are already a couple of terms we need to explain:\n\n## Terms explained\n\n### State\nAn immutable representation of the applications state. \nThe goal is to store the data in a way that is can be easily reused across multiple components. \n\nThe state is stored as a javascript object.\n\nThere is one global state across the whole angular application, but it is divided\ninto several sub-stores.\n\nA sub-store is available whenever there is an instance for the associated store-service.\nMore on this in the paragraph about store-services.\n\n### Action\nWhen an action is called it will dispatch an event with its payload to the action queue \nthat can be resolved by a Reducer or an Effect.\n\nActions can be called from components, services etc. \nIf there is no Reducer or Effect available, the action will simply be ignored.\n\n### Reducer\nWhen an action is dispatched it can be handled by a Reducer to mutate a part of the application state.\nThis function has the sole responsibility to manipulate and return a new instance of the edited state.\n\n- Reducers should be pure.\n- When Reducers manipulate state they must return a new object.\n- When Reducers not manipulate state they should return the injected state.\n\nA reducer is a function located in a store-service file. The function is decorated\nwith the Reducer() Decorator. In this decorator we link an action to this method:\n\n```ts\n@Reducer( CalculatorActions.add )\nonAdd( payload, state: CalculatorState ) {\n  return { currentValue: state.currentValue + payload.amount };\n}\n```\n\n### Effect\nWhen an action is dispatched it can be handled by an Effect to call a service or do any \nother side effect. When the effect is completed it can mutate the state by calling an Action()\nThe effect should return an Observable.\n\n- Effects are not allowed to directly manipulate state\n- Effects manipulate state via actions.\n- Effects are allowed to access variables in a service.\n- Effects are allowed to call services.\n- Effects are allowed to store information in the url.\n- Effects are allowed to retrieve information from the url.\n- Etc (everything non-pure)\n\nAn effect is a function located in a store-service file. The function is decorated\nwith the Effect() Decorator. In this decorator we link an action to this method:\n\n```ts\n@Effect( CalculatorActions.addFromUrl.start )\naddFromUrl( payload, state: CalculatorState ) {\n  return this.urlService.getNumberFromUrl().pipe(\n    tap((numberFromUrl) =\u003e CalculatorActions.addFromUrl.success(numberFromUrl))\n  );\n}\n```\n\nThe effect will be called when an action is dispatched. You normally don't call the effect directly. The effect can return an observable that will be subscribed to.\nYou don't have to unsubscribe from the observable, this is done automatically.\n\n### StoreAware\nA class decorator that allows a Component to connect to a Store based service.\nThis Decorator connects the Injected StoreService to a state property on the \ncomponent and makes sure that this property gets updated on every mutation of the state.\n\nThe component must be injected with the Store based service.\nEvery store based service requires a StoreAware decorator. When multiple storeServices \nare injected it is required to define the storeKey and the state Key param in the decorator.\nChanges to the assigned property are pushed trough ngOnChanges to make it possible to \nupdate component state when mutations to application state happen. \n\n```ts\n@Component({\n  ...\n})\n@StoreAware({ storeKey: 'storeService', stateKey: 'state' })\n@StoreAware({ storeKey: 'loadingStoreService', stateKey: 'loadingState' })\nexport class MyComponent {\n  protected storeService = inject(StoreService);\n  protected state!: BarrierControlBarrierDashboardInterface;\n\n  protected loadingStoreService = inject(LoadingStoreService);\n  protected loadingState!: LoadingStoreState;\n}\n```\n\n### Selector\nA pure function receives the current state of the application and optionally can receive parameters. \nIt is used to return a specific value that can be used in a component or another Selector.\n\n- Selectors receive at least the current state of the application.\n- Selectors are allowed to receive optional parameters that can be passed through via the component.\n- Selectors can return a specific key of the state, or calculate a derivative from the state\n- Selectors should be pure\n\n```ts\nstatic getPropertyFromState(state: MyState): number {\n    return state.property;\n  );\n}\n```\n\nThe reason to use selectors, and not directly access the state, is that the component or any other code \nshould not be aware of the structure of the state. This makes it easier to refactor the state without \nchanging the components.\n\n### CacheSelector\nSometimes a selector is more complex and is being called multiple times in a short period. In that case it can be useful to cache the result of the selector. This can be done by adding the CacheSelector decorator to the selector.\nThe cacheSelector decorator takes a function that returns a unique key for the selector. This key is used to cache the result of the selector. The key is calculated based on the state and the parameters passed to the selector.\n\n```ts\n@CacheSelector((state: CalculatorState) =\u003e state.number1 + state.number2)\nstatic getSum(state: CalculatorState): number {\n    return state.number1 + state.number2;\n  );\n}\n```\n\nAnother use of the CacheSelector is when a selector returns a unique array or object every time it is called. In that case it can be useful to cache the result of the selector to prevent unnecessary rerenders of the component.\n\n```ts\n@CacheSelector((state: MyState) =\u003e [...state.array, ...state.array2].map(item =\u003e item.id).join(''))\nstatic getArray(state: MyState): number[] {\n    return [...state.array, ...state.array2];\n  );\n}\n```\n\n## Helpers\nThe library provides a couple of helper functions to make it easier to work with the store.\n\n### ActionUtil.createActionWithSuccessAndFailure and EffectUtil.withActionHandlers\nThe createActionWithSuccessAndFailure and the withActionHandlers helper functions can be used together to create an action with a success and failure action, and handle loading as well.\nSo, oftentimes you have an effect that calls a service that can either succeed or fail. If you want to handle this consistently, without repeating the same code over and over, you can use these helper functions.\n\nWhen defining the action you can use the createActionWithSuccessAndFailure function to create an action with a success and failure action.\nInstead of having an action, say: 'fetchUser', the helper functions create four actions: 'fetchUser.start', 'fetchUser.success', 'fetchUser.error' and 'fetchUser.setStatus'\n\n```ts\n// my-store.actions.ts\nexport class TemplateEditorActions {\n  \nexport const MyActions = {\n  static fetchUser = ActionUtil.createActionWithSuccessAndFailure\u003cnumber, UserModel\u003e( 'MyActions/fetchUser' );\n};\n```\n\nSo now we have four actions that we can use in the store service for the reducers and effects:\n\n```ts\n// my-store.service.ts\n// ..\n\n@Effect( MyActions.fetchUser.start )\nfetchUser( payload: number ) {\n  return EffectUtil.withActionHandlers(\n    () =\u003e this.userService.getUser( payload ),\n    MyActions.fetchUser // \u003c-- return the action so that withActionHandlers can handle the success and failure\n  );\n}\n\n@Reducer( MyActions.fetchUser.success )\nonFetchUserSuccess( user: UserModel, state: MyState ) {\n  return { ...state, user };\n}\n```\n\nIt is also possible to change the result before passing it on to the success action:\n\n```ts\n@Effect( MyActions.fetchUser.start )\nfetchUser( payload: number ) {\n  return EffectUtil.withActionHandlers(\n    () =\u003e this.userService.getUser( payload ),\n    { \n      ...MyActions.fetchUser,\n      success: (response: Response\u003cUserModel\u003e) =\u003e MyActions.fetchUser.success(response[0]) // \u003c-- change the result before passing it on\n    }\n  );\n}\n```\n\nExamples of the setStatus and error actions:\n\n```ts\n@Reducer( MyActions.fetchUser.setStatus )\nonFetchUserSetStatus( status: EffectStatus, state: MyState ) {\n  return { ...state, fetchUserIsLoading: status === status.Pending };\n}\n\n@Effect( MyActions.fetchUser.error )\nfetchUserError() {\n  this.notifyService.error( 'Failed to fetch user' );\n}\n```\n\nIn the setStatus method you see that the type of status is EffectStatus. This is an enum that is provided by the library. The EffectStatus enum is used to represent the status of an effect in the state management system. It is defined in the core/lib/state-management/src/models/effect-status.model.ts file.  Here are the possible values for EffectStatus:  \nIDLE: This status indicates that the effect is idle and not currently in progress. It is the default status of an effect.  \nPENDING: This status indicates that the effect has been triggered and is currently in progress.  \nDONE: This status indicates that the effect has completed successfully.  \nERROR: This status indicates that an error occurred while executing the effect.  \n\n### ReducerUtil.setLoadingStatusKey\nBecause the loading status is a common pattern, the library provides a helper function to set the loading status key in the state. This function can be used as a reducer to set the loading status key in the state.\n\nIn the following example the loading status key is set to 'uploadDocumentStatus' when the action 'uploadDocument.setStatus' is dispatched.\n```ts\n// my-store.service\n// ..\n@Reducer(MyActions.uploadDocument.setStatus)\nuploadDocumentStatus = ReducerUtil.setLoadingStatusKey('uploadDocumentStatus');\n```\n\n## Debugging\nFrom the console it is possible to check which actions are fired or to check what data is in the store.\n\nTo see which actions are being fired type the following in the console, and hit \u003center\u003e\n```\n__ACTIONS.debugMode = 1; \n```\n\nYou can also check when the contents of the store changes:\n```\n__STATE.debugMode = 1\n```\n\nWhen you want to see the current contents of the store:\n```\n__STATE.snapshot\n```\n\n## Advanced usage\n\n### Limit the triggering of an effect\nSometimes an effect get triggered multiple times in a short period. In that case it can be useful to limit the triggering of the effect. \nMost of the times this happens when you have multiple actions that trigger the same effect, and you have no control when these actions are triggered.\n\nIn this case you can use the ConfigureEffectHandler decorator to limit the triggering of the effect. The decorator takes an object with the following properties:\n- takeLatest: boolean - If true, the effect will only be triggered when the last action is triggered. If false, the effect will be triggered for every action.\n- delayTime: number - The time in milliseconds that the effect will be delayed. If the effect is triggered multiple times in the delayTime, only the last effect will be triggered.\n\n```ts\n@Effect( MyActions.urlChange.start )\n@Effect( MyActions.buttonClick.start )\n@ConfigureEffectHandler({ takeLatest: true, delayTime: 100 })\nmyEffect( payload, state: MyState ) {\n  return this.urlService.getNumberFromUrl().pipe(\n    tap((numberFromUrl) =\u003e MyActions.urlChange.success(numberFromUrl))\n  );\n}\n```\n\n### Listen to an action outside the store service\nSometimes you want to listen to an action outside the store service. Most of the time this is not necessary, and you should use this with care.\nNormally you should handle actions inside the store service, but sometimes there are limitations you can't work around.\n\nIn this case you can use the ActionService to listen to an action. The ActionService is a singleton that can be used to listen to actions from anywhere in the application.\n\n```ts\n// Listen to an action in a component\nActionService.onAction$( TemplateActions.createNewTemplateDraftVersion.success )\n  .pipe( takeUntil( this.onDestroy$ ), delay( 100 ) ).subscribe( () =\u003e {\n    this.resetDraftChangeToDefault();\n    this.resetTemplateValues();\n} );\n```\n\n### Cancel an effect\nWhen an effect is triggered, it will run until it is completed. Sometimes you want to cancel an effect when a specific action is triggered. In this case you can use the takeUntil operator to cancel the effect.\n\n```ts\n@Effect( StoreActions.startExecutionOfRequest.start )\nstartExecution( payload: { property: number } ) {\n\n  return EffectUtil.withActionHandlers(\n    () =\u003e of( null )\n      .pipe(\n        delay( 100 ),\n        mergeMap( () =\u003e this.myService.getSomethingFromBackend( payload.property ) ),\n        mergeMap( ( result: MyModel ) =\u003e {\n          return this.myOtherService.getSomethingFromLocalStorage( result.id );\n        } ),\n        takeUntil( ActionUtil.onFirstAction$(\n          StoreActions.startOtherExecutionOfRequest.start\n        ) )\n      ),\n    {\n      ...StoreActions.startExecutionOfRequest,\n    }\n  );\n}\n```\n\n### Having multiple instances of a store \nDepending on how you want to structure your application there might be a need to have multiple instances of the same store.\nE.g. when having a component with a store that is used multiple times on the same page.\n\nThis is possible by providing the store service at the component level. This way the store service is only available for that component and its children.\nWhen multiple instances of the same store exists ngx-mxstore will make sure that the state of each store is kept separate, by giving it a unique name.\n\nBe aware that actions being triggered will be listened by all instances of the store. Sometimes this can be a good thing but oftentimes\nyou want a certain action only to be listened by a specific instance of the store.\n\nTo manage this you can give every instance of the store a unique name that set on the component level. Then give every\naction a context that is the same as the unique name of the store. In the effects and reducers you can check if the context\nis the same as the unique name of the store.\n\n## Testing\n\n### Reducers\nTesting reducers is easy. Just call the reducer with the current state and the payload and check the result.\n\n```ts\nit('should add 4 to the current value', () =\u003e {\n  const state = { currentValue: 5 };\n  const result = calculatorReducer.onAdd( { amount: 4 }, state );\n  expect( result.currentValue ).toBe( 9 );\n});\n```\n\n### Effects\nTesting effects is a bit more complex. You need to mock the services that are being called in the effect. You need to\nknow about the Angular TestBed and how to use it. Read more about this [[in the Angular documentation]](https://angular.io/guide/testing-services#angular-testbed).\n\nAlso you need to know about the jest spy functions. Read more about this [[in the jest documentation]](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname).\n\nIn this example we will use [[jest-generic-mock-class]](https://github.com/MaxxtonGroup/jest-generic-mock-class) to mock services.\n\n```ts\nimport { EffectTester } from 'ngx-mxstore';\nimport { GenericMockClass } from 'jest-generic-mock-class';\nimport { MyStoreService } from './my-store.service';\nimport { ApiService } from './api.service';\nimport { of } from 'rxjs';\n\ndescribe('MyStoreService', () =\u003e {\n  let service: MyStoreService;\n  let effectTester: EffectTester;\n  \n  const apiServiceMock: GenericMockClass\u003cApiService\u003e = GenericMockClass.create\u003cApiService\u003e({\n    functionToTest: jest.fn().mockReturnValue(of([{ id: 1, name: 'test' }])),\n  });\n  \n  beforeEach(() =\u003e {\n    TestBed.configureTestingModule({\n      providers: [\n        MyStoreService,\n        { provide: ApiService, useValue: apiServiceMock },\n      ],\n    });\n    service = TestBed.inject(MyStoreService);\n    effectTester = new EffectTester(service);\n  });\n  \n  it('should test an effect', (done) =\u003e {\n    effectTester\n      .expectEffect(MyStoreActions.doSomething.start)\n      .withState({\n        ...MyStoreService.initialState,\n      })\n      .toCallAction(MyStoreActions.doSomething.success)\n      .run(() =\u003e {\n        expect(apiServiceMock.getSpyFor('functionToTest')).toHaveBeenCalled();\n        done();\n      });\n  });\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMaxxtonGroup%2Fngx-mxstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMaxxtonGroup%2Fngx-mxstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMaxxtonGroup%2Fngx-mxstore/lists"}