{"id":19263085,"url":"https://github.com/mccallofthewild/ethos","last_synced_at":"2025-04-21T18:31:21.714Z","repository":{"id":54106445,"uuid":"86106528","full_name":"mccallofthewild/ethos","owner":"mccallofthewild","description":"Intuitive state management  ⚡️","archived":false,"fork":false,"pushed_at":"2021-03-09T09:41:25.000Z","size":295,"stargazers_count":3,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-13T19:14:21.929Z","etag":null,"topics":["computed","computed-properties","javascript","react","react-native","state-management"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/ethos","language":"JavaScript","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/mccallofthewild.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}},"created_at":"2017-03-24T20:07:17.000Z","updated_at":"2019-07-04T10:46:34.000Z","dependencies_parsed_at":"2022-08-13T06:50:36.215Z","dependency_job_id":null,"html_url":"https://github.com/mccallofthewild/ethos","commit_stats":null,"previous_names":["mccallofthewild/hivex"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mccallofthewild%2Fethos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mccallofthewild%2Fethos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mccallofthewild%2Fethos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mccallofthewild%2Fethos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mccallofthewild","download_url":"https://codeload.github.com/mccallofthewild/ethos/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250110942,"owners_count":21376561,"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":["computed","computed-properties","javascript","react","react-native","state-management"],"created_at":"2024-11-09T19:34:50.533Z","updated_at":"2025-04-21T18:31:21.344Z","avatar_url":"https://github.com/mccallofthewild.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ethos\nIntuitive state management.\n \n- - - -\n### Why Ethos?\n\n* **Intuitive**\n\t* Ethos is easy to learn and incrementally adoptable.\n* **Fast**\n\t* Not only can Ethos dramatically speed up your development process, it also beats Flux on benchmarks such as script evaluation, compile time and  lifecycle iteration.\n* **Powerful**\n\t* Ethos gives your data leverage with responsive features such as computed properties (`thoughts`) and watcher functions.\n- - - -\n\n### Getting Started\nThis tutorial will walk you through using Ethos with React.\n\n### Installation\n\n``` bash\n\tnpm install ethos --save\n```\nor \n``` bash\n\tyarn add ethos\n```\n\n## Principles\nEthos is built on the principle of a [Single Source of Truth](https://en.wikipedia.org/wiki/Single_source_of_truth).\nTo keep users mindful of this ideology, we’ve chosen to rename the popular `Store` and `state` to `Source` and `truth`. \n\n## Truth\nTruth is the most important property in the Ethos `Source`. It holds all the data.\n\n### Defining Truth\nDefining truth in Ethos is simple:\n``` javascript\n\t// ./source.js\n\nimport { Source } from 'ethos'\n\nlet count = 0;\nfunction id(){\n\tcount++\n\treturn count;\n}\nconst source = {\n\ttruth:{\n\t\ttodos:[\n\t\t\t{\n\t\t\t\ttext:\"take out the trash\",\n\t\t\t\tid:id(), //1\n\t\t\t\tcomplete:false,\n\t\t\t},\n\t\t\t{\n\t\t\t\ttext:\"clean room\",\n\t\t\t\tid:id(), //2\n\t\t\t\tcomplete:false,\n\t\t\t},\n\t\t\t{\n\t\t\t\ttext:\"feed dog\",\n\t\t\t\tid:id(), //3\n\t\t\t\tcomplete:false,\n\t\t\t}\n\t\t],\n\t\ttime:Date.now()\n\t}\n}\n\nexport default new Source(source);\n```\n### Accessing Truth\n\n#### source.getTruth()\n\u003e Truth is accessed outside the source by using a `Source` prototype method called `getTruth`  \n\n- `getTruth`  takes in two arguments:\n\t1. The first argument is a query for which `truth` properties you want. This can be an array or an object:\n\t\t-  With an *Array*, as in the example below, each string item represents both the name of your source's `truth` prop and the property it will be returned as. \n\t\t\t- e.g. `let localTruth = getTruth(['todos'], this)` can be used as `localtruth.todos` .\n\t\t- With an  *Object*, you can alias a source's truth properties with whatever you want by using a key of your custom name with a value of the actual property name.\n\t\t\t- e.g. If you wanted `'todos'` to be aliased as `'myTodos'`, you could use  `let localTruth = getTruth({ myTodos: 'todos' })`  then reference it as `localTruth.myTodos` .\n2. The second argument is the component itself, `this`.  It essentially tells Ethos to watch the component and update it when something changes.\n\nFull Example:\n\n``` javascript \n// ./my-component.js\nimport React from 'react';\nimport source from './source.js';\n\nexport default class TodoList extends React.Component {\n\tconstructor(props){\n\t\t\tsuper(props);\n\t\t\tthis.truth = source.getTruth(['todos'], this);\n\t}\n\t\n\trender(){\n\t\treturn (\n\t\t\t\u003cul id=\"todo-list\"\u003e\n\t\t\t\t{this.truth.todos.map(\n\t\t\t\t\t(todo, index)=\u003e (\n\t\t\t\t\t\t\u003cli\n\t\t\t\t\t\t\tkey={todo.id}\n\t\t\t\t\t\t\u003e\n\t\t\t\t\t\t{todo.text}\n\t\t\t\t\t\t\u003c/li\u003e\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t\u003c/ul\u003e\n\n\t\t)\n\t}\n}\n```\n\u003e Note that `getTruth` returns an object of getters, so `Object.assign` and the *object rest spread operator* will not work with the returned object.  \n\n## Writers\n\u003e This is great, but `truth`  is constantly changing. In Ethos, truth is updated with `writers`.  \n\nThe formatting for `writers` isn't much different than `truth`, but there's a bit more going on here:\n``` javascript\n\t// ./source.js\n\t\n\timport { Source } from 'ethos'\n\n  let count = 0;\n  function id(){\n    count++;\n    return count;\n  }\n\tconst source = {\n\t\ttruth:{...}, // Same as above\n\n\t\twriters:{\n\t\t\taddTodo(text){\n\t\t\t\tlet todo = { \n\t\t\t\t\ttext:text, \n\t\t\t\t\tid:id(),\n\t\t\t\t\tcomplete:false,\n\t\t\t\t}\n\t\t\t\tthis.truth.todos.push(todo);\n\t\t\t},\n\t\t\tcompleteTodo(index){\n\t\t\t\tlet todo = this.truth.todos[index];\n\t\t\t\ttodo.complete = true;\n\t\t\t}\n\t\t},\n\t}\n\n\texport default new Source(source);\n```\n\n### What’s `this`?\n\u003e To avoid some pains of other systems, Ethos binds your `writers` to a snapshot of your `Source`. This makes it possible for writer functions to accept as many arguments as necessary.   \n\n- `this.truth` is your `Source`’s `truth` property, there for you to access and change it as you please.\n- `this.writers` are your `Source`’s writers. \n- `this.runners` are your `Source`’s runners.  (more on this in a bit)\n- `this.write` is your `Source`’s  `write` method. 〃                    〃\n- `this.run` is your `Source`’s `run` method.          〃                    〃\n\n### Running Writers\n\nThe easiest way to invoke a writer is to access it  in `source.writers`.\n\n``` javascript \n\t// ./my-component.js\n\timport React from 'react';\n\timport source from './source.js';\n\n\texport default class TodoList extends React.Component {\n\t\tconstructor(props){\n\t\t   super(props);\n\t\t    this.truth = source.getTruth(['todos'], this)\n\t\t}\n\n\t\taddTodo(text){\n\t\t\tsource.writers.addTodo(text)\n\t\t}\n\t\t\n\t\tcompleteTodo(index){\n\t\t\tsource.writers.completeTodo(index)\n\t\t}\n\n\t\trender(){\n\t\t\treturn (\n\n\t\t\t\u003cul id=\"todo-list\"\u003e\n\t\t\t\t{this.truth.todos.map((todo, index)=\u003e (\n\t\t\t\t\t\u003cli\n\t\t\t\t\t\tkey={todo.id}\n\t\t\t\t\t\tonClick={()=\u003ethis.completeTodo(index)}\n\t\t\t\t\t\u003e\n\t\t\t\t\t{todo.text}\n\t\t\t\t\u003c/li\u003e\n\t\t\t\t))}\n\t\t\t\u003c/ul\u003e\n\n\t\t\t)\n\t\t}\n\t}\n```\n\nThere’s another way to invoke a writer: the `write` method. \n`source.write`  takes in two arguments. The first is the writer’s name and the second is the argument you want to pass to the writer. \n\nHence,  `addTodo`  above could be rewritten as\n``` javascript\n...\n\t\taddTodo(text){\n\t\t\tsource.write('addTodo', text)\n\t\t}\n...\n```\n\nBoth methods provide the same functionality. Using `write`, however, limits you to one argument.  The latter method may look a bit more familiar if you’re coming from flux/redux.\n\n## Runners\n\u003e Writers have one catch: they update your components synchronously. This means asynchronous changes ( made via  API calls, WebSockets, or `setTimeout`s, etc. ) may not have updated `truth` by the time Ethos updates your components.  \n\nTo solve this problem, we have `runners`. Ethos `runners` handle all asynchronous activity in the `source`. Put simply, `runners` *run* other functions.\n\nYou may have noticed we already have a `time` property in the `truth` of our example. Let’s make it update once per second. \n``` javascript\n\t// ./source.js\n\t\n\timport { Source } from 'ethos'\n\t\n\tconst source = {\n\t\ttruth:{\n\t\t\ttodos:[...], // Same as above\n\t\t\ttime:Date.now()\n\t\t},\n\t\t\n\t\twriters:{\n\t\t\t... // Same as above\n\t\t\tupdateTime(){\n\t\t\t\tthis.truth.time = Date.now();\n\t\t\t}\n\t\t},\n\t\t\t\n\t\trunners:{\n\t\t\tinitTime(){\n\t\t\t\tlet timeout = setInterval(()=\u003e{\n\t\t\t\t\t/* this will run once per second */\n\t\t\t\t\tthis.writers.updateTime();\n\t\t\t\t}, 1000)\n\t\t\t}\n\t\t}\n\t\t\n\t}\n\n\texport default new Source(source);\n```\n\n### What’s `this`?\n\u003e Similarly to `writers`, `runners` are bound to a snapshot representing functionality in your `Source`. Runners’ snapshot is slightly different, however.  \n\n- `this.writers` are your `Source`’s writers. \n- `this.runners` are your `Source`’s runners. \n- `this.write` is your `Source`’s  `write` method.\n- `this.run` is your `Source`’s  `run` method. ( we’ll get to this in a second )\n\n#### Truth \u0026 Done\n\u003e While runners also have access to `truth` , any mutations made to truth will not sync without use of the `done` method.  \n*  `this.truth` is your `Source`’s `truth`.\n*  `this.done` is a method which tells your source that you mutated `truth`, and the `source` needs to update accordingly. \n\nThis enables you to avoid writing tedious `writers` which simply change a value. \n\nSee an example of `this.done()` in **Examples** below.\n\u003e `this.done` is an experimental feature and disabling it will be possible with the upcoming `strict` mode.  \n\n#### Promise Wrappers\n\u003e Ethos also gives you the ability to wrap any runner in an ES6 Promise using `this.async()`, `this.resolve()` and `this.reject()`.  \nPromises can get quite verbose. Promise wrappers aim to fix that. \n* `this.async()`  is the method which initializes the Promise wrapper. It must be invoked *outside* your asynchronous code.\n* `this.resolve()` is the Promise’s *resolve* function.\n* `this.reject()` is the Promise’s *reject* function.\nSee an example of Promise wrappers in **Examples** below.\n\n### Running Runners\nNow, our `initTime` function won’t run itself. (though technically, it could 🙃)  \n\nThe easiest way to invoke a runner is to access it  in `source.runners`.\n``` javascript \n\tsource.runners.initTime()\n```\n\nJust like with writers, there’s another way to invoke a runner: the `run` method. \n`source.run`  takes in two arguments. The first is the function name and the second is the payload, a lone object. \n\nHence,  the above code could also be written as\n``` javascript\n...\n\tsource.run('initTime')\n...\n```\nThe same principles apply for `run` as those for `write`.\n\n#### Examples \n\nMutating truth with `this.done()`\n``` javascript \n\t...\n\t\trunners:{\n\t\t\tinitTime(){\n\t\t\t\tlet timeout = setInterval(()=\u003e{\n\t\t\t\t\t/* this will run once per second */\n\t\t\t\t\tthis.truth.time = Date.now();\n\t\t\t\t\tthis.done()\n\t\t\t\t}, 1000)\n\t\t\t}\n\t\t}\n\t...\n```\n\nUsing **Promise Wrappers**\n\u003e This example handles a simple GET request to the [Giphy API](https://github.com/Giphy/GiphyAPI)  using the popular HTTP client, [Axios](https://github.com/mzabriskie/axios).  \n``` javascript \n\t...\n\trunners:{\n\t\tgetRandomGifUrl(){\n        /*\n        1. Initialize the Promise wrapper *outside* the\n           asynchronous code.\n        */\n        this.async();\n\n        let baseUrl = 'http://api.giphy.com/v1/gifs/random';\n\n\t\t  axios.get(baseUrl + '?api_key=dc6zaTOxFJmzC\u0026tag=ethos')\n\t\t  .then((res)=\u003e{\n            let imageUrl = res.data.data.image_url;\n            // resolves promise\n            this.resolve(imageUrl);\n\t\t  })\n        .catch((error)=\u003e{\n          // rejects promise\n          this.reject(error);\n        })\n\t\t}\n\t}\n\t...\n```\nNow when `getRandomGifUrl` runs, it will return a Promise. The following will be possible:\n``` javascript\n\tlet defaultImageUrl = 'https://media.giphy.com/media/UbQs9noXwuSFa/giphy.gif?response_id=591ccaaaecadb1fa9e03044c'\n\tsource.runners.getRandomGifUrl()\n\t.then((imageUrl)=\u003e{\n\t\t/* \n\t\timagine you have a function which changes the \n\t\tsource of an image\n\t\t*/\n\t\tsetImageSrc(imageUrl)\n\t})\n\t.catch((error)=\u003e{\n\t\tsetImageSrc(defaultImageUrl)\n\t})\n```\nIn many cases, using `async` and `await` is the optimal path, but Promise wrappers are nice for when your asynchronous code doesn’t already utilize promises.\n\n## Watchers\nA `watcher` is a function that is invoked whenever a property on `truth` changes.\nWatchers are defined like so: \n``` javascript\n\t// ./source.js\n\t\n\timport { Source } from 'ethos'\n\t\n\tconst source = {\n\t\ttruth:{\n\t\t\ttodos:[...], // Same as above\n\t\t\ttime:Date.now()\n\t\t},\n\t\t\n\t\twriters:{...}, // Same as above\n\t\t\t\n\t\trunners:{...}, // Same as above\n\t\t\n\t\twatchers:{\n\t\t\ttodos(){\n\t\t\t\t/* \n\t\t\t\tthis will run every time \n\t\t\t\tsomething changes in `truth.todos`\n\t\t\t\t*/\n\t\t\t\tconsole.log('Todos changed!')\n\t\t\t}\n\t\t}\n\t}\n\n\texport default new Source(source);\n```\n\n### What’s `this`?\n\u003e `this` for `watchers` is the same as `this` for `writers`  \n- `this.truth` is your `Source`’s truth property. \n\t- It’s not suggested that you directly mutate `truth` from watchers.\n- `this.writers` are your `Source`’s writers. \n- `this.runners` are your `Source`’s runners.\n- `this.write` is your `Source`’s  `write` method.\n- `this.run` is your `Source`’s `run` method.\n\n## Thoughts\n\u003e Thoughts observe one or more pieces of `truth`, combine it with some custom logic, and return a new piece of `truth`. When a piece of `truth` a thought is observing changes, the thought will update its value.  \nLet’s say we have two numbers, `a` and `b`, in our `truth`.\n\n``` javascript\n...\n      truth:{\n        a:1,\n        b:2,\n      },\n      writers:{\n        addOneToA(){\n          this.truth.a = this.truth.a+1;\n        }\n      },\n      thoughts:{\n        sum(){\n          return this.truth.a + this.truth.b;\n        }\n      }\n...\n\n```\n\nat this point, we can access `sum` like so:\n```javascript\n\t// ./my-component.js\n...\n\n\tlet localTruth = source.getTruth(['sum', 'a', 'b'], this)\n\t// localTruth.a is 1\n\t// localTruth.b is 2\n\t// localTruth.sum is 3\n\tif( localTruth.sum == (localTruth.a + localTruth.b) ){\n\t\tconsole.log('Ethos is legit.')\n\t}\n\n...\n\t\n```\nbut if we changed `truth.a`…\n``` javascript\n\t// ./my-component.js\n...\n\n\tsource.writers.addOneToA()\n\t// localTruth.a is 2\n\t// localTruth.b is 2\n\t// localTruth.sum is 4\n\n\tif( localTruth.sum == (localTruth.a + localTruth.b) ){\n\t\tconsole.log('Redux who?')\n\t}\n\n...\n```\n\n### What’s `this`?\n\u003e `this` for `thoughts` is the same as `this` for `writers`  \n- `this.truth` is your `Source`’s truth property. \n\t- It’s not suggested that you directly mutate `truth` from `thoughts`\n- `this.writers` are your `Source`’s writers. \n- `this.runners` are your `Source`’s runners.\n- `this.write` is your `Source`’s  `write` method.\n- `this.run` is your `Source`’s `run` method.\n\n## Founder Function\n\u003e In an Ethos Source, the `founder` function is a function that is instantly invoked once the store is built. It can be used to initialize a lot of store functionality an avoid contaminating your view layer with store logic.  \n\nExample:\n```javascript \n...\n\n    truth:{...},\n    writers:{...},\n\t\trunners:{...},\n    thoughts:{...},\n\n\t\tfounder(){\n\t\t\tthis.runners.authenticateUser();\n\t\t\tthis.runners.openSockets();\n\t\t}\n\n...\n```\n\n### What’s `this`?\n\u003e `this` for the `founder` function is the same as `this` for `writers`  \n- `this.truth` is your `Source`’s truth property. \n\t- It’s not suggested that you directly mutate `truth` from the `founder` function.\n- `this.writers` are your `Source`’s writers. \n- `this.runners` are your `Source`’s runners.\n- `this.write` is your `Source`’s  `write` method.\n- `this.run` is your `Source`’s `run` method.\n\n## Children\n\u003e To organize your sources, Ethos has `children`. Each child is its own independent source.  \n\nChild sources are defined like so:\n``` javascript\nimport {\n\tSource,\n} from 'ethos'\n\nconst source = {\n\t\ttruth:{...},\n\t\twriters:{...},\n\n\t\tchildren:{\n\t\t// children are named by the property they are nested under\n\t\t\t\tusers:{ // a source just for your users\n\n\t\t\t\t\t\ttruth:{\n\t\t\t\t\t\t\tcurrentUser:{\n\t\t\t\t\t\t\t\temail:'',\n\t\t\t\t\t\t\t\tfirstname:'',\n\t\t\t\t\t\t\t\tlastname:'',\n\t\t\t\t\t\t\t\tid:''\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\n\t\t\t\t\t\tthoughts:{\n\t\t\t\t\t\t\tfullName(){\n\t\t\t\t\t\t\t\tlet user = this.truth.currentUser;\n\t\t\t\t\t\t\t\treturn user.firstname + user.lastname;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\n\t\t\t\t\t\tchildren:{ // nested children\n\t\t\t\t\t\t\tfriends:{...}\n\t\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t}\n}\n\nexport default new Source(source);\n```\n\nAccess children on a source like so:\n``` javascript\n\tlet userSource = source.child('users')\n\tlet userTruth = userSource.getTruth(['currentUser'], this)\n```\n\nAccess nested children one of two ways:\n1. chaining `child` methods\n\t``` javascript\n\tsource.child('users').child('friends')\n\t```\n2. Query string\n\t``` javascript\n\tsource.child('users.friends')\n\t```\n\nRunners, writers, thoughts, watchers and the founder function all have additional properties on  `this`  to access parent and child sources.\n* `this.child()` is the source’s child method, same as above.\n* `this.parent` is the source’s parent source.\n* `this.origin` is the source’s origin source ( the one directly constructed with `new Source`)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmccallofthewild%2Fethos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmccallofthewild%2Fethos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmccallofthewild%2Fethos/lists"}