# 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.
- Ethos gives your data leverage with responsive features such as computed properties (
Getting Started
This tutorial will walk you through using Ethos with React.
Installation
npm install ethos --save
or
yarn add ethos
Principles
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.jslet count = 0;{count++return count;}const source =truth:todos:text:"take out the trash"id: //1complete:falsetext:"clean room"id: //2complete:falsetext:"feed dog"id: //3complete:falsetime:Date;
Accessing Truth
source.getTruth()
Truth is accessed outside the source by using a
Source
prototype method calledgetTruth
getTruth
takes in two arguments:- The first argument is a query for which
truth
properties 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's
truth
prop and the property it will be returned as.- e.g.
let localTruth = getTruth(['todos'], this)
can be used aslocaltruth.todos
.
- e.g.
- With an Array, as in the example below, each string item represents both the name of your source's
- 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 first argument is a query for which
- 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;;Component{superprops;thistruth = source;}{return<ul id="todo-list">thistruthtodos</ul>}
Note that
getTruth
returns an object of getters, soObject.assign
and the object rest spread operator will not work with the returned object.
Writers
This is great, but
truth
is 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.jslet count = 0;{count++;return count;}const source =truth:... // Same as abovewriters:{let todo =text:textid:complete:falsethistruthtodos;}{let todo = thistruthtodosindex;todocomplete = true;};
this
?
What’s To avoid some pains of other systems, Ethos binds your
writers
to a snapshot of yourSource
. This makes it possible for writer functions to accept as many arguments as necessary.
this.truth
is yourSource
’struth
property, there for you to access and change it as you please.this.writers
are yourSource
’s writers.this.runners
are yourSource
’s runners. (more on this in a bit)this.write
is yourSource
’swrite
method. 〃 〃this.run
is yourSource
’srun
method. 〃 〃
Running Writers
The easiest way to invoke a writer is to access it in source.writers
.
// ./my-component.js;;Component{superprops;thistruth = source}{sourcewriters}{sourcewriters}{return<ul id="todo-list">thistruthtodos</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
...{source}...
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
setTimeout
s, etc. ) may not have updatedtruth
by 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.jsconst source =truth:todos:... // Same as abovetime:Datewriters:... // Same as above{thistruthtime = Date;}runners:{let timeout =};
this
?
What’s Similarly to
writers
,runners
are bound to a snapshot representing functionality in yourSource
. Runners’ snapshot is slightly different, however.
this.writers
are yourSource
’s writers.this.runners
are yourSource
’s runners.this.write
is yourSource
’swrite
method.this.run
is yourSource
’srun
method. ( 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 thedone
method.
this.truth
is yourSource
’struth
.this.done
is a method which tells your source that you mutatedtruth
, and thesource
needs 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.done
is an experimental feature and disabling it will be possible with the upcomingstrict
mode.
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
.
sourcerunners
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...
The same principles apply for run
as those for write
.
Examples
Mutating truth with this.done()
...runners:{let timeout =}...
Using Promise Wrappers
This example handles a simple GET request to the Giphy API using the popular HTTP client, Axios.
...runners:{/*1. Initialize the Promise wrapper *outside* theasynchronous code.*/this;let baseUrl = 'http://api.giphy.com/v1/gifs/random';axios}...
Now when getRandomGifUrl
runs, it will return a Promise. The following will be possible:
let defaultImageUrl = 'https://media.giphy.com/media/UbQs9noXwuSFa/giphy.gif?response_id=591ccaaaecadb1fa9e03044c'sourcerunners
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.jsconst source =truth:todos:... // Same as abovetime:Datewriters:... // Same as aboverunners:... // Same as abovewatchers:{/*this will run every timesomething changes in `truth.todos`*/console};
this
?
What’s
this
forwatchers
is the same asthis
forwriters
this.truth
is yourSource
’s truth property.- It’s not suggested that you directly mutate
truth
from watchers.
- It’s not suggested that you directly mutate
this.writers
are yourSource
’s writers.this.runners
are yourSource
’s runners.this.write
is yourSource
’swrite
method.this.run
is yourSource
’srun
method.
Thoughts
Thoughts observe one or more pieces of
truth
, combine it with some custom logic, and return a new piece oftruth
. When a piece oftruth
a thought is observing changes, the thought will update its value. Let’s say we have two numbers,a
andb
, in ourtruth
.
...truth:a:1b:2writers:{thistrutha = thistrutha+1;}thoughts:{return thistrutha + thistruthb;}...
at this point, we can access sum
like so:
// ./my-component.js... let localTruth = source // localTruth.a is 1 // localTruth.b is 2 // localTruth.sum is 3 if localTruthsum == localTrutha + localTruthb console ...
but if we changed truth.a
…
// ./my-component.js...sourcewriters// localTruth.a is 2// localTruth.b is 2// localTruth.sum is 4if localTruthsum == localTrutha + localTruthbconsole...
this
?
What’s
this
forthoughts
is the same asthis
forwriters
this.truth
is yourSource
’s truth property.- It’s not suggested that you directly mutate
truth
fromthoughts
- It’s not suggested that you directly mutate
this.writers
are yourSource
’s writers.this.runners
are yourSource
’s runners.this.write
is yourSource
’swrite
method.this.run
is yourSource
’srun
method.
Founder Function
In an Ethos Source, the
founder
function 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:... { thisrunners; thisrunners; }...
this
?
What’s
this
for thefounder
function is the same asthis
forwriters
this.truth
is yourSource
’s truth property.- It’s not suggested that you directly mutate
truth
from thefounder
function.
- It’s not suggested that you directly mutate
this.writers
are yourSource
’s writers.this.runners
are yourSource
’s runners.this.write
is yourSource
’swrite
method.this.run
is yourSource
’srun
method.
Children
To organize your sources, Ethos has
children
. Each child is its own independent source.
Child sources are defined like so:
const source =truth:...writers:...children:// children are named by the property they are nested underusers: // a source just for your userstruth:currentUser:email:''firstname:''lastname:''id:''thoughts:{let user = thistruthcurrentUser;return userfirstname + userlastname;}children: // nested childrenfriends:...;
Access children on a source like so:
let userSource = sourcelet userTruth = userSource
Access nested children one of two ways:
- chaining
child
methods
source
- Query string
source
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.parent
is the source’s parent source.this.origin
is the source’s origin source ( the one directly constructed withnew Source
)