ethos v0.0.1
# Ethos
Intuitive state management for React.
Why Ethos?
- Intuitive * Ethos is easy to learn and incrementally adoptable.
- Fast * 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.
- Powerful
* Ethos gives your data leverage with responsive features such as computed properties (
thoughts) and watcher functions.
Getting Started
This tutorial will walk you through using Ethos with React.
Installation
npm install ethos --saveor
yarn add ethosPrinciples
Ethos is built on the principle of a Single Source of Truth.
To keep users mindful of this ideology, we’ve chosen to rename the popular Store and state to Source and truth.
Truth
Truth is the most important property in the Ethos Source. It holds all the data.
Defining Truth
Defining truth in Ethos is simple:
// ./source.js
import Ethos, { Source } from 'ethos'
let count = 0;
function id(){
count++
return count;
}
const source = {
truth:{
todos:[
{
text:"take out the trash",
id:id(), //1
complete:false,
},
{
text:"clean room",
id:id(), //2
complete:false,
},
{
text:"feed dog",
id:id(), //3
complete:false,
}
],
time:Date.now()
}
}
export default new Source(source);Accessing Truth
source.getTruth()
Truth is accessed outside the source by using a
Sourceprototype method calledgetTruth
getTruthtakes in two arguments: 1. The first argument is a query for whichtruthproperties you want. This can be an array or an object: - With an Array, as in the example below, each string item represents both the name of your source'struthprop and the property it will be returned as. - e.g.let localTruth = getTruth(['todos'], this)can be used aslocaltruth.todos. - 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. - e.g. If you wanted'todos'to be aliased as'myTodos', you could uselet localTruth = getTruth({ myTodos: 'todos' })then reference it aslocalTruth.myTodos.
- The second argument is the component itself,
this. It essentially tells Ethos to watch the component and update it when something changes.
Full Example:
// ./my-component.js
import React from 'react';
import source from './source.js';
export default class TodoList extends React.Component {
constructor(props){
super(props);
this.truth = source.getTruth(['todos'], this);
}
render(){
return (
<ul id="todo-list">
{this.truth.todos.map((todo, index)=> (
<li
key={todo.id}
>
{todo.text}
</li>
))}
</ul>
)
}
}Note that
getTruthreturns an object of getters, soObject.assignand the object rest spread operator will not work with the returned object.
Writers
This is great, but
truthis constantly changing. In Ethos, truth is updated withwriters.
The formatting for writers isn't much different than truth, but there's a bit more going on here:
// ./source.js
import Ethos, { Source } from 'ethos'
let count = 0;
function id(){
count++;
return count;
}
const source = {
truth:{...}, // Same as above
writers:{
addTodo(text){
let todo = {
text:text,
id:id(),
complete:false,
}
this.truth.todos.push(todo);
},
completeTodo(index){
let todo = this.truth.todos[index];
todo.complete = true;
}
},
}
export default new Source(source);What’s this?
To avoid some pains of other systems, Ethos binds your
writersto a snapshot of yourSource. This makes it possible for writer functions to accept as many arguments as necessary.
this.truthis yourSource’struthproperty, there for you to access and change it as you please.this.writersare yourSource’s writers.this.runnersare yourSource’s runners. (more on this in a bit)this.writeis yourSource’swritemethod. 〃 〃this.runis yourSource’srunmethod. 〃 〃
Running Writers
The easiest way to invoke a writer is to access it in source.writers.
// ./my-component.js
import React from 'react';
import source from './source.js';
export default class TodoList extends React.Component {
constructor(props){
super(props);
this.truth = source.getTruth(['todos'], this)
}
addTodo(text){
source.writers.addTodo(text)
}
completeTodo(index){
source.writers.completeTodo(index)
}
render(){
return (
<ul id="todo-list">
{this.truth.todos.map((todo, index)=> (
<li
key={todo.id}
onClick={()=>this.completeTodo(index)}
>
{todo.text}
</li>
))}
</ul>
)
}
}There’s another way to invoke a writer: the write method.
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.
Hence, addTodo above could be rewritten as
...
addTodo(text){
source.write('addTodo', text)
}
...Both 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.
Runners
Writers have one catch: they update your components synchronously. This means asynchronous changes ( made via API calls, WebSockets, or
setTimeouts, etc. ) may not have updatedtruthby the time Ethos updates your components.
To solve this problem, we have runners. Ethos runners handle all asynchronous activity in the source. Put simply, runners run other functions.
You may have noticed we already have a time property in the truth of our example. Let’s make it update once per second.
// ./source.js
import Ethos, { Source } from 'ethos'
const source = {
truth:{
todos:[...], // Same as above
time:Date.now()
},
writers:{
... // Same as above
updateTime(){
this.truth.time = Date.now();
}
},
runners:{
initTime(){
let timeout = setInterval(()=>{
/* this will run once per second */
this.writers.updateTime();
}, 1000)
}
}
}
export default new Source(source);What’s this?
Similarly to
writers,runnersare bound to a snapshot representing functionality in yourSource. Runners’ snapshot is slightly different, however.
this.writersare yourSource’s writers.this.runnersare yourSource’s runners.this.writeis yourSource’swritemethod.this.runis yourSource’srunmethod. ( we’ll get to this in a second )
Truth & Done
While runners also have access to
truth, any mutations made to truth will not sync without use of thedonemethod.
this.truthis yourSource’struth.this.doneis a method which tells your source that you mutatedtruth, and thesourceneeds to update accordingly.
This enables you to avoid writing tedious writers which simply change a value.
See an example of this.done() in Examples below.
this.doneis an experimental feature and disabling it will be possible with the upcomingstrictmode.
Promise Wrappers
Ethos also gives you the ability to wrap any runner in an ES6 Promise using
this.async(),this.resolve()andthis.reject(). Promises can get quite verbose. Promise wrappers aim to fix that.
this.async()is the method which initializes the Promise wrapper. It must be invoked outside your asynchronous code.this.resolve()is the Promise’s resolve function.this.reject()is the Promise’s reject function. See an example of Promise wrappers in Examples below.
Running Runners
Now, our initTime function won’t run itself. (though technically, it could 🙃)
The easiest way to invoke a runner is to access it in source.runners.
source.runners.initTime()Just like with writers, there’s another way to invoke a runner: the run method.
source.run takes in two arguments. The first is the function name and the second is the payload, a lone object.
Hence, the above code could also be written as
...
source.run('initTime')
...The same principles apply for run as those for write.
Examples
Mutating truth with this.done()
...
runners:{
initTime(){
let timeout = setInterval(()=>{
/* this will run once per second */
this.truth.time = Date.now();
this.done()
}, 1000)
}
}
...Using Promise Wrappers
This example handles a simple GET request to the Giphy API using the popular HTTP client, Axios.
... runners:{ getRandomGifUrl(){ /* 1. Initialize the Promise wrapper *outside* the asynchronous code. */ this.async();
let baseUrl = 'http://api.giphy.com/v1/gifs/random';
axios.get(baseUrl + '?api_key=dc6zaTOxFJmzC&tag=ethos')
.then((res)=>{
let imageUrl = res.data.data.image_url;
// resolves promise
this.resolve(imageUrl);
})
.catch((error)=>{
// rejects promise
this.reject(error);
})
}
}
...Now when `getRandomGifUrl` runs, it will return a Promise. The following will be possible:
``` javascript
let defaultImageUrl = 'https://media.giphy.com/media/UbQs9noXwuSFa/giphy.gif?response_id=591ccaaaecadb1fa9e03044c'
source.runners.getRandomGifUrl()
.then((imageUrl)=>{
/*
imagine you have a function which changes the
source of an image
*/
setImageSrc(imageUrl)
})
.catch((error)=>{
setImageSrc(defaultImageUrl)
})In 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.
Watchers
A watcher is a function that is invoked whenever a property on truth changes.
Watchers are defined like so:
// ./source.js
import Ethos, { Source } from 'ethos'
const source = {
truth:{
todos:[...], // Same as above
time:Date.now()
},
writers:{...}, // Same as above
runners:{...}, // Same as above
watchers:{
todos(){
/*
this will run every time
something changes in `truth.todos`
*/
console.log('Todos changed!')
}
}
}
export default new Source(source);What’s this?
thisforwatchersis the same asthisforwriters
this.truthis yourSource’s truth property. - It’s not suggested that you directly mutatetruthfrom watchers.this.writersare yourSource’s writers.this.runnersare yourSource’s runners.this.writeis yourSource’swritemethod.this.runis yourSource’srunmethod.
Thoughts
Thoughts observe one or more pieces of
truth, combine it with some custom logic, and return a new piece oftruth. When a piece oftrutha thought is observing changes, the thought will update its value. Let’s say we have two numbers,aandb, in ourtruth.
...
truth:{
a:1,
b:2,
},
writers:{
addOneToA(){
this.truth.a = this.truth.a+1;
}
},
thoughts:{
sum(){
return this.truth.a + this.truth.b;
}
}
...at this point, we can access sum like so:
// ./my-component.js
...
let localTruth = source.getTruth(['sum', 'a', 'b'], this)
// localTruth.a is 1
// localTruth.b is 2
// localTruth.sum is 3
if( localTruth.sum == (localTruth.a + localTruth.b) ){
console.log('Ethos is legit.')
}
...
but if we changed truth.a…
// ./my-component.js
...
source.writers.addOneToA()
// localTruth.a is 2
// localTruth.b is 2
// localTruth.sum is 4
if( localTruth.sum == (localTruth.a + localTruth.b) ){
console.log('Redux who?')
}
...What’s this?
thisforthoughtsis the same asthisforwriters
this.truthis yourSource’s truth property. - It’s not suggested that you directly mutatetruthfromthoughtsthis.writersare yourSource’s writers.this.runnersare yourSource’s runners.this.writeis yourSource’swritemethod.this.runis yourSource’srunmethod.
Founder Function
In an Ethos Source, the
founderfunction is a function that is instantly invoked once the source is built. It can be used to initialize a lot of source functionality an avoid contaminating your view layer with data logic.
Example:
...
truth:{...},
writers:{...},
runners:{...},
thoughts:{...},
founder(){
this.runners.authenticateUser();
this.runners.openSockets();
}
...What’s this?
thisfor thefounderfunction is the same asthisforwriters
this.truthis yourSource’s truth property. - It’s not suggested that you directly mutatetruthfrom thefounderfunction.this.writersare yourSource’s writers.this.runnersare yourSource’s runners.this.writeis yourSource’swritemethod.this.runis yourSource’srunmethod.
Children
To organize your sources, Ethos has
children. Each child is its own independent source.
Child sources are defined like so:
import {
Source,
} from 'ethos'
const source = {
truth:{...},
writers:{...},
children:{
// children are named by the property they are nested under
users:{ // a source just for your users
truth:{
currentUser:{
email:'',
firstname:'',
lastname:'',
id:''
}
},
thoughts:{
fullName(){
let user = this.truth.currentUser;
return user.firstname + user.lastname;
}
},
children:{ // nested children
friends:{...}
}
}
}
}
export default new Source(source);Access children on a source like so:
let userSource = source.child('users')
let userTruth = userSource.getTruth(['currentUser'], this)Access nested children one of two ways:
1. chaining child methods
source.child('users').child('friends')- Query string
source.child('users.friends')Runners, writers, thoughts, watchers and the founder function all have additional properties on this to access parent and child sources.
this.child()is the source’s child method, same as above.this.parentis the source’s parent source.this.originis the source’s origin source ( the one directly constructed withnew Source)
8 years ago