{"id":27101422,"url":"https://github.com/couzic/lenrix","last_synced_at":"2025-07-03T05:04:32.974Z","repository":{"id":22909599,"uuid":"96795816","full_name":"couzic/lenrix","owner":"couzic","description":"Type-safe, reactive, focusable redux store wrapper","archived":false,"fork":false,"pushed_at":"2023-06-12T07:39:40.000Z","size":817,"stargazers_count":22,"open_issues_count":28,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-12T01:46:25.413Z","etag":null,"topics":["epics","immutable","lens","redux","rxjs","state-management","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/couzic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-07-10T15:58:23.000Z","updated_at":"2025-03-20T21:12:08.000Z","dependencies_parsed_at":"2022-07-27T09:22:27.468Z","dependency_job_id":null,"html_url":"https://github.com/couzic/lenrix","commit_stats":{"total_commits":190,"total_committers":4,"mean_commits":47.5,"dds":"0.13684210526315788","last_synced_commit":"b2f8ab03c79619eeaf7f57764079d228cb12a3fd"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/couzic/lenrix","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/couzic%2Flenrix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/couzic%2Flenrix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/couzic%2Flenrix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/couzic%2Flenrix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/couzic","download_url":"https://codeload.github.com/couzic/lenrix/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/couzic%2Flenrix/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263264642,"owners_count":23439247,"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","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":["epics","immutable","lens","redux","rxjs","state-management","typescript"],"created_at":"2025-04-06T14:36:48.505Z","updated_at":"2025-07-03T05:04:32.942Z","avatar_url":"https://github.com/couzic.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# lenrix\n\n#### 🔎 Lenses + Redux + RxJS + TypeScript = ❤️ \n\n## Table of Contents\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n\n- [Motivation](#motivation)\n- [Features](#features)\n- [Quickstart](#quickstart)\n  - [Install](#install)\n- [API](#api)\n  - [Create](#create)\n    - [`createStore()`](#createstore)\n    - [`createFocusableStore()`](#createfocusablestore)\n  - [Actions and Updates](#actions-and-updates)\n    - [`actionTypes()`](#actiontypes)\n    - [`updates()`](#updates)\n    - [`dispatch()`](#dispatch)\n    - [`action()`](#action)\n  - [Consuming the state](#consuming-the-state)\n    - [`state$`](#state)\n    - [`currentState`](#currentstate)\n    - [`pluck()`](#pluck)\n    - [`pick()`](#pick)\n    - [`cherryPick()`](#cherrypick)\n  - [Focus](#focus)\n    - [`focusPath()`](#focuspath)\n    - [`focusFields()`](#focusfields)\n    - [`recompose()`](#recompose)\n    - [Passing fields as readonly values](#passing-fields-as-readonly-values)\n  - [Computed values (synchronous)](#computed-values-synchronous)\n    - [`compute()`](#compute)\n    - [`computeFromField()`](#computefromfield)\n    - [`computeFromFields()`](#computefromfields)\n    - [`computeFrom()`](#computefrom)\n  - [Computed values (asynchronous)](#computed-values-asynchronous)\n    - [`compute$()`](#compute)\n    - [`computeFromField$()`](#computefromfield)\n    - [`computeFromFields$()`](#computefromfields)\n    - [`computeFrom$()`](#computefrom)\n    - [`defaultValues()`](#defaultvalues)\n  - [`epics()`](#epics)\n  - [`pureEpics()`](#pureepics)\n  - [`sideEffects()`](#sideeffects)\n  - [Injected `store`](#injected-store)\n- [Testing](#testing)\n  - [Test Setup](#test-setup)\n    - [Root store](#root-store)\n  - [Asserting state](#asserting-state)\n  - [Asserting calls on dependencies](#asserting-calls-on-dependencies)\n  - [Asserting dispatched actions](#asserting-dispatched-actions)\n- [Logger](#logger)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Motivation\n\nA lot of people have complained about `redux`, some with good reason. Many have been drawn to other state management solutions.\n\n\u003e Don't throw the baby with the bathwater.\n\nAlthough we agree there must be a better way than classical `redux`, we are not willing to sacrifice all of the `redux` goodness you've heard so much about.\n\n\u003e Making redux great again !\n\n`lenrix` is a `redux` store wrapper that :\n - Dramatically reduces boilerplate\n - Eliminates the need for thunky middleware and selector libraries\n - Makes no compromise on type-safety\n - Embraces reactive programming\n - Prevents unnecessary re-rendering\n\n## Features\n\n - Declarative API for deep state manipulation, powered by [`immutable-lens`](https://github.com/couzic/immutable-lens)\n - Reactive state and selectors, powered by [`rxjs`](https://github.com/reactivex/rxjs)\n - Relevant reference equality checks performed out of the box\n - Separate functional slices with [Focused Stores](#focus)\n - Epics, just like [`redux-observable`](https://github.com/redux-observable/redux-observable), our favorite redux middleware\n\n## Quickstart\n\n### Install\n```bash\nnpm install --save lenrix redux rxjs immutable-lens\n```\n\n**`rootStore.ts`**\n```ts\nimport { createStore } from 'lenrix'\n\nconst initialRootState = {\n   message: ''\n}\n\nexport const rootStore = createStore(initialRootState)\n      .actionTypes\u003c{ // DECLARE ACTION AND PAYLOAD TYPES\n         setMessage: string\n      }\u003e()\n      .updates({ // REGISTER UPDATES (~= CURRIED REDUCERS)\n         setMessage: (message) =\u003e (state) =\u003e ({...state, message})\n      }))\n```\n\n**`storeConsumer.ts`**\n```ts\nimport { rootStore } from './rootStore'\n\nconst message$ = rootStore.pluck('message') // Observable\u003cstring\u003e \nconst slice$ = rootStore.pick('message') // Observable\u003c{message: string}\u003e\n\nrootStore.dispatch({setMessage: 'Hello !!!'})\n```\n\n## API\n\n### Create\n\n#### `createStore()`\n```ts\nimport {createStore} from 'lenrix'\n\nconst rootStore = createStore({\n   user: {\n      id: 123,\n      name: 'John Doe'\n   }\n})\n```\n\n#### `createFocusableStore()`\nProvides the same API as `createStore()` from `redux`.\n```ts\nimport {createFocusableStore} from 'lenrix'\n\nconst initialRootState = { ... }\n\nexport type RootState = typeof initialRootState\n\nexport const store = createFocusableStore(\n   (state: RootState) =\u003e state, // You can use your old redux reducer here\n   initialRootState,\n   (window as any).__REDUX_DEVTOOLS_EXTENSION__ \u0026\u0026 (window as any).__REDUX_DEVTOOLS_EXTENSION__()\n)\n```\n\n### Actions and Updates\n\n#### `actionTypes()`\nDeclare the store's actions and associated payload types. Calling this method will have absolutely no runtime effect, all it does is provide information to the TypeScript compiler.\n```ts\nconst store = createStore({name: 'Bob'})\n   .actionTypes\u003c{\n      setName: string\n   }\u003e()\n```\n\n#### `updates()`\nOnce action types are defined, it is possible to register type-safe updates. See [`immutable-lens`](https://github.com/couzic/immutable-lens) for `lens` API documentation.\n```ts\nconst store = createStore({name: 'Bob'})\n   .actionTypes\u003c{setName: string}\u003e()\n   // THESE FOUR CALLS TO updates() ARE ALL EQUIVALENT AND 100% TYPE SAFE\n   // PICK THE ONE YOU PREFER\n   .updates({\n      setName: (name) =\u003e (state) =\u003e ({...state, name})\n   })\n   .updates(lens =\u003e ({\n      setName: (name) =\u003e lens.setFields({name})\n   }))\n   .updates(lens =\u003e ({\n      setName: (name) =\u003e lens.focusPath('name').setValue(name)\n   }))\n   // And if really like curry...\n   .updates(lens =\u003e ({\n      setName: lens.focusPath('name').setValue()\n   }))\n```\n**Only *ONE* updater can be registered for a single action type.** Failing to comply with that rule will result in an error:\n```\nError: Cannot register two updaters for the same action type\n```\nWhen an action needs to update the state at a wider scope, move your updater to a store that has larger focus. \n\n#### `dispatch()`\nDispatching an action can trigger an [update](#updates), an [epic](#epics), or a [side effect](#sideEffects).\n```ts\nstore.dispatch({setName: 'John'}) // Next state will be : {name: 'John'}\n```\n\n#### `action()`\nCreate an action dispatcher, which can be handily used in a `React` component for example.\n```ts\nconst setName = store.action('setName')\nsetName('John') // Same as store.dispatch({setName: 'John'})\n```\n\n### Consuming the state\n`lenrix` performs reference equality checks to prevent any unnecessary re-rendering.\n\nThe store provides the properties `state$` and `currentState`. However, we recommend you to use either `pluck()`, `pick()` or `cherryPick()` to select as little data as necessary. It will prevent components to re-render because an irrelevant slice of the state has changed.\n\n#### `state$`\nThe store's normalized state augmented with its readonly values.\n```ts\nconst store = createStore({name: 'Bob'})\n\nstore.state$ // Observable\u003c{name: string}\u003e \n```\n\n#### `currentState`\nHandy for testing.\n```ts\nconst store = createStore({name: 'Bob'})\n\nstore.currentState.name // 'Bob' \n```\n\n#### `pluck()`\nConceptually equivalent to `focusPath(...).state$`\n```ts\nconst rootStore = createStore({\n   user: {\n      name: 'Bob'\n   }\n})\n\nconst userName$ = rootStore.pluck('user', 'name') // Observable\u003cstring\u003e \n```\n\n#### `pick()`\nConceptually equivalent to `focusFields().state$`\n```ts\nconst rootStore = createStore({\n   counter: 0,\n   user: 'Bob',\n   todoList: ['Write README']\n})\n\nconst pick$ = rootStore.pick(\n   'user',\n   'todoList'\n) // Observable\u003c{ user: string, todoList: string[] }\u003e\n```\n\n#### `cherryPick()`\nConceptually equivalent to `recompose().state$`. See [`immutable-lens`](https://github.com/couzic/immutable-lens) for lens API documentation.\n```ts\nconst rootStore = createStore({\n   counter: 0,\n   user: {\n      name: 'Bob'\n   }\n})\n\nconst cherryPick$ = rootStore.cherryPick(lens =\u003e ({ // immutable-lens\n   counter: lens.focusPath('counter'),\n   userName: lens.focusPath('user', 'name')\n})) // Observable\u003c{ counter: number, userName: string }\u003e\n```\n\n### Focus\n\n**A focused store is just a proxy for the root store; there always is a single source of truth.**\n\nMost UI components only interact with a small part of the whole state tree. A focused store provides read and update access to a precise subset of the full state.\n\nTypically, you will create a focused store for a specific page (1st level routable component). Then, if the page is functionnally rich, more stores can be derived from the page-focused store. These deep-focused stores will probably be tailored for specific components or groups of components within the page.\n\nAll these stores form a tree of stores, with the one returned by `createStore()` at its root. All dispatched actions are propagated to the root store, where updates are applied. The updated state then flows down the tree of stores, with [values computed](#computed-values-synchronous) at some of the nodes and made available to their children.\n\nHowever, that propagation stops at the stores for which the state slice in their scope has not changed.\n\n\n#### `focusPath()`\n```ts\nconst rootStore = createStore({\n   user: {\n      id: 123,\n      name: 'John Doe'\n   }\n})\n\nconst userStore = rootStore.focusPath('user')\nconst userNameStore = userStore.focusPath('name')\n// OR\nconst userNameStore = rootStore.focusPath('user', 'name')\n\nuserNameStore.state$ // Observable\u003cstring\u003e\n```\n\n#### `focusFields()`\n```ts\nconst rootStore = createStore({\n   counter: 0,\n   username: 'Bob'\n})\n\nconst counterStore = rootStore.focusFields('counter')\n\ncounterStore.state$ // Observable\u003c{counter: number}\u003e \n```\n\n#### `recompose()`\nMost powerful focus operator. It allows you to create state representations composed of deep properties from distinct state subtrees. See [`immutable-lens`](https://github.com/couzic/immutable-lens) for lens API documentation.\n```ts\nconst rootStore = createStore({\n   a: {\n      b: {\n         c: {\n            d: string\n         }\n      }\n   },\n   e: {\n      f: {\n         g: {\n            h: string\n         }\n      }\n   }\n})\n\nrootStore\n   .recompose(lens =\u003e ({ // immutable-lens\n      d: lens.focusPath('a', 'b', 'c', 'd'),\n      h: lens.focusPath('e', 'f', 'g', 'h')\n   }))\n   .state$ // Observable\u003c{ d: string, h: string }\u003e\n```\n\n#### Passing fields as readonly values\nAll three focus operators `focusPath()`, `focusFields()` and `recompose()` support passing fields as readonly values.\n```ts\nconst rootStore = createStore({\n   a: {\n      b: {\n         c: 'c'\n      }\n   },\n   user: 'Bob'\n})\n\nconst focusedStore = rootStore.focusPath(['a', 'b', 'c'], ['user'])\nfocusedStore.state$ // Observable\u003c{ c: string, user: string }\u003e\n```\nNote that in this example, updates registered on the focused store can't modify the `user` value\n\n### Computed values (synchronous)\nState should be normalized, derived data should be declared as computed values. In traditional redux, you would probably use selectors for that.\n\n`lenrix` performs reference equality checks to prevent unnecessary recomputation.\n\n#### `compute()`\n```ts\ncreateStore({name: 'Bob'})\n   .compute(state =\u003e ({message: 'Hello, ' + state.name}))\n   .pick('message') // Observable\u003c{message: string}\u003e\n```\n\n#### `computeFromField()`\nSpecify the field used for the computation to avoid useless re-computations.\n```ts\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFromField(\n      'name',\n      name =\u003e ({message: 'Hello, ' + name})\n   )\n   .pick('message') // Observable\u003c{message: string}\u003e\n```\n\n#### `computeFromFields()`\nSpecify the fields used for the computation to avoid useless re-computations.\n```ts\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFromFields(\n      ['name'],\n      ({name}) =\u003e ({message: 'Hello, ' + name})\n   )\n   .pick('message') // Observable\u003c{message: string}\u003e\n```\n\n#### `computeFrom()`\nDefine computed values from state slices focused by lenses. The signature is similar to `recompose()` and `cherryPick()`.\n```ts\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFrom(\n      lens =\u003e ({name: lens.focusPath('name')}),\n      ({name}) =\u003e ({message: 'Hello, ' + name}))\n   .pick('message') // Observable\u003c{message: string}\u003e\n```\n### Computed values (asynchronous)\nEvery synchronous value-computing operator has an asynchronous equivalent.\n\nNote that asynchronously computed values are initially undefined. If you want them to be non-nullable, see [`defaultValues()`](#defaultValues()).\n\n#### `compute$()`\n```ts\nimport { map, pipe } from 'rxjs/operators'\n\ncreateStore({name: 'Bob'})\n   .compute$(\n      // WITH SINGLE OPERATOR...\n      map(state =\u003e ({message: 'Hello, ' + state.name}))\n      // ... OR WITH MULTIPLE OPERATORS\n      pipe(\n         map(state =\u003e state.name),\n         map(name =\u003e ({message: 'Hello, ' + name}))\n      )\n   )\n   .pick('message') // Observable\u003c{message: string | undefined}\u003e\n```\n#### `computeFromField$()`\n```ts\nimport { map } from 'rxjs/operators'\n\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFromField$(\n      'name',\n      map(name =\u003e ({message: 'Hello, ' + name}))\n   )\n   .pick('message') // Observable\u003c{message: string | undefined}\u003e\n```\n#### `computeFromFields$()`\n```ts\nimport { map } from 'rxjs/operators'\n\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFromFields$(\n      ['name'],\n      map(({name}) =\u003e ({message: 'Hello, ' + name}))\n   )\n   .pick('message') // Observable\u003c{message: string | undefined}\u003e\n```\n#### `computeFrom$()`\n```ts\nimport { map } from 'rxjs/operators'\n\ncreateStore({name: 'Bob', irrelevant: 'whatever'})\n   .computeFrom$(\n      lens =\u003e ({name: lens.focusPath('name')}),\n      map(({name}) =\u003e ({message: 'Hello, ' + name}))\n   .pick('message') // Observable\u003c{message: string | undefined}\u003e\n```\n\n#### `defaultValues()`\nDefine defaults for read-only values.\n```ts\nimport { map } from 'rxjs/operators'\n\ncreateStore({name: 'Bob'})\n   .compute$(\n      map(({name}) =\u003e ({message: 'Hello, ' + name}))\n   )\n   .defaultValues({\n      message: ''\n   })\n   .pick('message') // Observable\u003c{message: string}\u003e\n```\n\n### `epics()`\nLet an action dispatch another action, asynchronously. Since this feature is heavily inspired from [`redux-observable`](https://github.com/redux-observable/redux-observable), we encourage you to go check their [documentation](https://redux-observable.js.org/docs/basics/Epics.html).\n```ts\nimport { pipe } from 'rxjs'\nimport { map } from 'rxjs/operators'\n\ncreateStore({name: '', message: ''})\n   .actionTypes\u003c{\n      setName: string\n      setMessage: string\n   }\u003e()\n   .updates(lens =\u003e ({\n      setName: name =\u003e lens.setFields({name}),\n      setMessage: message =\u003e lens.setFields({message})\n   }))\n   .epics(store =\u003e ({\n      // WITH SINGLE OPERATOR...\n      setName: map(name =\u003e ({setMessage: 'Hello, ' + name}))\n      // ... OR WITH MULTIPLE OPERATORS\n      setName: pipe(\n         map(name =\u003e 'Hello, ' + name),\n         map(message =\u003e ({setMessage: message}))\n      )\n   }))\n```\n\n### `pureEpics()`\nSame as [`epics()`](#epics()), without the [injected `store`](#injected-store) instance.\n```ts\n...\n   .pureEpics({\n      setName: map(name =\u003e ({setMessage: 'Hello, ' + name}))\n   })\n```\n\n### `sideEffects()`\nDeclare synchronous side effects to be executed in response to actions. Useful for pushing to browser history, stuff like that...\n```ts\ncreateStore({ name: '' })\n   .actionTypes\u003c{\n      setName: string \n   }\u003e()\n   .updates(lens =\u003e ({\n      setName: name =\u003e lens.setFields({name})\n   }))\n   .sideEffects({\n      setName: name =\u003e console.log(name)\n   })\n```\n\n### Injected `store`\n\nA light version of the store is made available when using the following operators :\n - [`compute$()`](#compute$)\n - [`computeFrom$()`](#computeFrom$)\n - [`computeFromFields$()`](#computeFromFields$)\n - [`epics()`](#epics)\n - [`sideEffects()`](#sideEffects)\n\n```ts\nimport { mapTo } from 'rxjs/operators'\n\ncreateStore({name: '', greeting: ''})\n   .actionTypes\u003c{\n      setName: string\n      setGreeting: string\n   }\u003e()\n   .updates(lens =\u003e ({\n      setName: name =\u003e lens.setFields({name}),\n      setGreeting: greeting =\u003e lens.setFields({greeting})\n   }))\n   .epics(store =\u003e ({\n      setName: map(() =\u003e ({setGreeting: store.currentState.name}))\n   }))\n```\n\n## Testing\n\n\u003e Testing an action creator, a reducer and a selector in isolation.\n\n![Man in three pieces. Legs running in place. Torso doing push-ups. Head reading.](https://cdn-images-1.medium.com/max/1600/0*eCs8GoVZVksoQtQx.gif)\n\u003e \"Looks like it’s working !\"\n\nTesting in `redux` usually implies testing in isolation the pieces that together form the application's state management system. It seems reasonable, since they are supposed to be pure functions.\n\nTesting in `lenrix` follows a different approach. Well, technically, in most cases it would still be possible to write tests the `redux` way, but that's not what we had in mind when we designed it.\n\nA `lenrix` store is to be considered a cohesive **unit** of functionality. We want to **test it as a whole**, by interacting with its public API. We do not want to test its internal implementation details.\n\nAs a consequence, we believe store testing should essentially consist in :\n- [Dispatching actions](#dispatch)\n- [Asserting state](#asserting-state) (normalized state + computed values)\n\nIn some less frequent cases, testing might also consist in :\n- [Asserting calls on dependencies](#asserting-calls-on-dependencies)\n- [Asserting dispatched actions](#asserting-dispatched-actions)\n\n### Test Setup\nEach test should run in isolation, therefore we need to create a new store for each test. The most straightforward way is to wrap all store creation code in factory functions.\n\n#### Root store\n**`RootStore.ts`**\n```ts\nimport { createStore } from 'lenrix'\n\nexport const initialRootState = {\n   user: {\n      name: ''\n   }\n}\n\nexport type RootState = typeof initialRootState\n\nexport const createRootStore = (initialState = initialRootState) =\u003e createStore(initialState)\n\nexport type RootStore = ReturnType\u003ctypeof createRootStore\u003e\n```\n\n**`RootStore.spec.ts`**\n```ts\nimport 'jest'\nimport { createRootStore, RootStore } from './RootStore'\n\ndescribe('RootStore', () =\u003e {\n   let store: RootStore\n\n   beforeEach(() =\u003e {\n      store = createRootStore()\n   })\n})\n```\n\n### Asserting state\nMost tests should limit themselves to dispatching actions and verifying that the state has correctly updated.\n\nThe distinction between normalized state and readonly values should be kept hidden as an implementation detail. Tests should not make assumptions about a value being either readonly or part of the normalized state, as it is subject to change without breaking public API nor general behavior.\n\n**`RootStore.ts`**\n```ts\nimport { createStore } from 'lenrix'\n\nexport const createRootStore = (initialState = {name: ''}) =\u003e createStore(initialState)\n   .actionTypes\u003c{\n      setName: string\n   }\u003e()\n   .updates(lens =\u003e ({\n      setName: (name) =\u003e lens.setFields({name})\n   }))\n   .compute(({name}) =\u003e ({\n      message: 'Hello, ' + name\n   }))\n\nexport type RootStore = ReturnType\u003ctypeof createRootStore\u003e\n```\n\n**`RootStore.spec.ts`**\n```ts\nimport 'jest'\nimport { createRootStore, RootStore } from './RootStore'\n\ndescribe('RootStore', () =\u003e {\n   let store: RootStore\n\n   beforeEach(() =\u003e {\n      store = createRootStore()\n   })\n\n   test('updates name when \"setName\" dispatched', () =\u003e {\n      store.dispatch({setName: 'Bob'})\n\n      expect(store.currentState.name).toEqual('Bob')\n   })\n\n   test('updates message when \"setName\" dispatched', () =\u003e {\n      store.dispatch({setName: 'Steve'})\n\n      expect(store.currentState.message).toEqual('Hello, Steve')\n   })\n})\n```\n\n### Asserting calls on dependencies\n\n### Asserting dispatched actions\n\n## Logger\n\n\u003c!-- \nConsider a `redux` store. It's an object holding the application's whole state, which can be a massive, complex and deep JavaScript plain object. Such complexity can be hard to maintain, so tricks like `combineReducers()` have been invented to allow some kind of concern separation. The store maintains not only the whole state, but also all the operations that can make changes to that state, in the form of reducers (well, technically there's only one reducer, but let's not bother with the technical details right now). So a store is really :\n\u003e state + operations to change the state\n\nLooks a lot like an object to me (as in Object-Oriented Programming). A store has a well encapsulated state, the only way to change it is to interact with the store's public API. It even has a typical `getState()` accessor. State of the art OOP !\n\nOf course there are major differences between a classical object and a `redux` store, I'm just trying to point out that conceptually, a store can be thought of as a an object with state and behavior. And how do you test an object with state and behavior ? Here's the first thing I can think of :\n\u003e Call a method and make assertions on the state\n\nLet's translate this in `redux` lingo :\n\u003e Dispatch an action and make assertions on the state\n\nI hear you asking : \"Isn't it the same thing as testing the reducer in isolation ?\"\n\nIn simple cases, yes, the outcome would be identical. However, in real-life situations where middleware is involved, a single action can trigger a chain of actions. Dispatching an action would effectively trigger the middleware, testing the reducer in isolation would not.\n--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcouzic%2Flenrix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcouzic%2Flenrix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcouzic%2Flenrix/lists"}