{"id":19446028,"url":"https://github.com/nerdgeschoss/redux-database","last_synced_at":"2025-04-25T01:31:52.863Z","repository":{"id":32771052,"uuid":"130725040","full_name":"nerdgeschoss/redux-database","owner":"nerdgeschoss","description":"Simple reducer based in-memory database with strong typings and immutable documents, with plugins for redux and react.","archived":false,"fork":false,"pushed_at":"2023-01-06T01:57:47.000Z","size":2077,"stargazers_count":16,"open_issues_count":17,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-22T07:41:26.124Z","etag":null,"topics":["database","immutable","react","redux","redux-store","state"],"latest_commit_sha":null,"homepage":"https://nerdgeschoss.github.io/redux-database","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nerdgeschoss.png","metadata":{"files":{"readme":"Readme.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-23T16:21:49.000Z","updated_at":"2023-08-17T19:30:30.000Z","dependencies_parsed_at":"2023-01-14T22:15:13.192Z","dependency_job_id":null,"html_url":"https://github.com/nerdgeschoss/redux-database","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdgeschoss%2Fredux-database","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdgeschoss%2Fredux-database/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdgeschoss%2Fredux-database/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdgeschoss%2Fredux-database/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nerdgeschoss","download_url":"https://codeload.github.com/nerdgeschoss/redux-database/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250738071,"owners_count":21479128,"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":["database","immutable","react","redux","redux-store","state"],"created_at":"2024-11-10T16:12:33.033Z","updated_at":"2025-04-25T01:31:52.533Z","avatar_url":"https://github.com/nerdgeschoss.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redux Database\n\n![Tests](https://github.com/nerdgeschoss/redux-database/workflows/Tests/badge.svg?branch=master)\n\nSimple reducer based in-memory database with strong typings and immutable documents, with plugins for redux and react.\n\n## Why do I need this?\n\nClient side data normalization is hard. This library helps you to organize your state in a relational way, with queries\nand joins as you would expect from an SQL based database. There is a storage adaper for redux or you can use it as a\nstandalone library (redux-database has no dependencies!).\n\nIf you wish to use this library with React, you can easily observe mutations on the state via subscriptions and hooks.\n\n## Defining your state\n\nDefine your models (only requirement is a required string `id` on them) and a corresponding database schema. The `settings`\nkey is for key-value-based data (e.g. UI settings, session tokens, etc) while the `data` key is for relational table data.\n\n```ts\ninterface Thing {\n  id: string;\n  name: string;\n}\n\ninterface State {\n  settings: {\n    enableAwesomeThing: boolean;\n  };\n  data: {\n    things: DataTable\u003cThing\u003e;\n  };\n}\n```\n\nCreate your initial state:\n\n```ts\nimport { emptyTable } from 'redux-database';\n\nconst state: State = {\n  settings: {\n    enableAwesomeThing: true,\n  },\n  data: {\n    things: emptyTable,\n  },\n};\n```\n\n## Reading Data\n\nYou can create a data snapshot from the state (and manage the state via redux or with the supplied `MutableDB`).\n\n```ts\nconst db = new DB(state);\n```\n\n### Reading Key/Value Settings\n\nKey-Value-Settings are type-safe. Typescript will tell if you if type an invalid key or use the wrong type.\n\n```ts\ndb.get('enableAwesomeThing'); // true\n```\n\n### Reading Records\n\nRecords are organized in tables, referenced by their name. Again Typescript will tell if you mistyped anything.\n\n```ts\nconst things = db.table('things'); // if things is not defined, you would get an error here\nthings.all; // returns Thing[]\nthings.find('12'); // find by id\nthings.where({ name: 'tool' }); // simple equality based where queries\nthings.where((thing) =\u003e thing.name.length == 4); // function based where queries\n```\n\n### Chainable Queries\n\nIf you prefer to work with the chainable query syntax, you modify search results during a query:\n\n```ts\ndb\n  .query('things')\n  .where({ name: 'tool' })\n  .select('name')\n  .order({ name: 'desc' }).first; // returns { id: '12', name: 'tool' }\n```\n\nQueries allow embedding records from other table. Assuming there is a `userId` on `things` pointing to a `users` table:\n\n```ts\n// embed rows from `users`, selected by `userId` under the key `user`\ndb.query('things').embed('user', 'users', 'userId').first; // returns { id: '12', name: 'tool', user: { id: '1', name: 'User' } }\n```\n\nIf there is an array of ids (e.g. `userIds`), there's a corresponding `embedMulti` method to embed collections of rows.\n\n## Writing Data\n\nData is never written directly. Instead all writer methods return a redux action that can be handled by the included reducer.\n\n### Writing Settings\n\n```ts\nstore.dispatch(db.set('enableAwesomeThing', false));\n```\n\n### Writing Records\n\n```ts\nstore.dispatch(things.insert({ name: 'New Thing' })); // ids get assigned automatically if not provided\nstore.dispatch(things.update('1', { name: 'New Name' })); // the first parameter could be an array instead for multi updates\nstore.dispatch(things.delete('1'));\n```\n\nWriting multiple records this way will update your UI multiple times - this can lead to performance problems, especially when updating and inserting huge amounts of data. For this you can use a transaction:\n\n```ts\nstore.dispatch(\n  db.transaction((dispatch) =\u003e {\n    dispatch(things.insert({ name: 'First Thing' }));\n    dispatch(things.insert({ name: 'Second Thing' }));\n  })\n);\n```\n\nThis way there is only a single action dispatched to your redux store that encapsulates all the changes.\n\n## Working with Contexts\n\nContexts work as a scratchpad to draft changes before you commit them to the main database. You can imagine them as a writable overlay over your database:\n\n```ts\nconst draftDB = db.context('draft');\nstore.dispatch(draftDB.table('things').update('1', { name: 'Updated Thing' }));\n// retrieve your state again from the store\ndb.table('things').first.name; // this is still 'First Thing'\ndraftDB.table('things').first.name; // this is updated to 'Updated Thing'\n```\n\nTo get a list of everything that will be changed by the context, use the corresponding methods:\n\n```ts\ndraftDB.table('things').changesFor('1'); // { deleted: false, inserted: false, changes: { name: 'Updated Thing' } }\ndraftDB.table('things').changes; // an array of change objects for every row that has changed\n```\n\nWhen you're done with changes, you can commit them to the main store:\n\n```ts\nstore.dispatch(draftDB.commit());\n```\n\nYou can also nest contexts and commit them to their parent context:\n\n```ts\nconst draftDB = db.context('local').context('draft');\nstore.dispatch(draftDB.table('things').update('1', { name: 'Updated Thing' }));\nstore.dispatch(draftDB.commit());\n// retrieve your state again from the store\ndb.table('things').first.name; // this is still 'First Thing'\ndraftDB.context('local').table('things').first.name; // this is now the value from the nested context\n```\n\n## Mutable Databases\n\nAs you've seen so far, `DB` always works on a snapshot of data, bringing it in line with Redux. Sometimes it's beneficial though to always have a getter for live data. This is where `MutableDB` comes in:\n\n```ts\nconst db = new MutableDB(state);\ndb.set('enableAwesomeThing', false);\ndb.get('enableAwesomeThing'); // now is false\n\nconst things = db.table('things');\nthings.insert({ name: 'First Thing' });\nthings.where({ name: 'First Thing' }); // =\u003e retrieves your record\n```\n\nYou can always get an immutable snapshot of the current state:\n\n```ts\ndb.snapshot; // a DB instance of the current state\n```\n\nThis database keeps the state internal and automatically uses the included reducer. If you have a Redux store (this is completely option), you can synchronize it with the `store` option:\n\n```ts\nimport { store } from 'redux';\nimport { MutableDB, reducer } from 'redux-database';\n\nconst store = createStore(reducer(state), state);\nconst db = new MutableDB(state, { store }); // automatically synchronized with the store\n```\n\nThis will automaticall keep the database instance in sync with your redux store and vice versa.\n\n### Observing Changes\n\n`MutableDB`s allow observing them for changes via subscriptions.\n\n```ts\nconst observation = db.observe((snapshot) =\u003e snapshot.query(things).first);\nobservation.current; // always returns the current value\nobservation.subscribe((value) =\u003e console.log(value)); // Notifies if the result of the query has changed. This performs a deep equality check.\nobservation.query = (db) =\u003e db.query(things).last; // queries can be changed at runtime. This will also call all observers.\nobservation.cancel(); // to discard an observation once you're done\n```\n\n## React Integration\n\nA full integration package with debugging tools is under development, until then you can simply integrate via hooks:\n\n```ts\n// define a hook to force a component to rerender:\nexport function useForceUpdate(): () =\u003e void {\n  const [, updateState] = useState(true);\n  return () =\u003e {\n    updateState((state) =\u003e !state);\n  };\n}\n\n// define a hook to use your database:\nexport function useDatabase\u003cT\u003e(query: (db: DB\u003cState\u003e) =\u003e T): T {\n  const forceUpdate = useForceUpdate();\n  const subscriptionRef = useRef\u003cSubscription\u003cState, T\u003e\u003e();\n  if (!subscriptionRef.current) {\n    subscriptionRef.current = mutableDB.observe(query);\n    subscriptionRef.current.subscribe(() =\u003e forceUpdate());\n  }\n  subscriptionRef.current.query = query;\n  useEffect(() =\u003e {\n    return () =\u003e subscriptionRef.current.cancel();\n  }, []);\n  return subscriptionRef.current.current;\n}\n```\n\nNow you can use this hook in your app to retrieve data and react will update your component if the data changes (and only, if it changes):\n\n```ts\nfunction ThingsList(): JSX.Element {\n  const things = useDatabase(db =\u003e db.query('things').ordered(name: 'asc').all); // `things` is strongy typed here!\n  return \u003cdiv\u003e{things.map(e =\u003e e.name).join()}\u003c/div\u003e\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnerdgeschoss%2Fredux-database","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnerdgeschoss%2Fredux-database","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnerdgeschoss%2Fredux-database/lists"}