drborges / arbor-store Goto Github PK
View Code? Open in Web Editor NEWSeamless state management made with ❤️
Home Page: https://github.com/drborges/arbor-store
Seamless state management made with ❤️
Home Page: https://github.com/drborges/arbor-store
The idea here is simple, allow developers to provide their own abstraction to a given state tree path. Take the following Store
as example:
const store = new Store({
todos: [
{ title: "Do the dishes", status: "todo" },
{ title: "Take the dog out", status: "doing" },
{ title: "Do the laundry", status: "done" },
]
})
One could then create a class to represent a todo and even provide a more domain specific API:
@Model("/todos/:index")
class Todo {
isStarted() {
return this.status === "doing"
}
start() {
if (this.status === "todo") this.status = "doing"
}
finish() {
if (this.status === "doing") this.status = "done"
}
}
This model can be made visible to the store by "registering" it as so:
import { Todo } from "./models"
store.register(Todo)
With that, whenever a todo is accessed, an instance of Todo
would be provided:
const todo = store.todos.find(todo => !todo.isStarted())
todo.start()
Arbor uses a few different caches in order to optimize memory usage and speed things up a bit. For example, there is always a single instance of Path
for a given sequence of path nodes, e.g. new Path("users", 0) === new Path("users", 0)
.
Each proxy within a PTree
has a cache to hold their children so that they are only created once (upon first access) for a given state tree, e.g. multiple accesses to the users
node (tree.root.users
) create the users
proxy as a child of root
on the first access, subsequent accesses yield the cached proxy.
Using WeakMap
for these caches would (likely, benchmark might be needed) allow the GC to free up memory when no other object in the app is referencing these cached items.
This would essentially allow one to "rollback" mutations to a certain path. For optimistic UIs this means we'd be able to easily rollback UI changes upon API request failures:
store.subscribe("/users/:index", async (newUser, oldUser, restore) => {
const response = await api.users.update(newUser)
if (response.status >= 400) {
restore(oldUser).errors.push(response.message)
}
})
Subscribers would receive a restore
function as the third argument, to be called with the previous value of the mutated node, returning the restored node allowing further method chaining.
This is React
specific concern and it should be provided by a separate package. Decoupling arbor from React allows it to be used in non-react apps (angular, amber, vue, vanilla js).
index.js
files only used internally by the libcore.js
and babel-runtime
from the final packages.Currently, subscribers are notified with only the new state. The old state should also be provided so that subscribers may choose to, perhaps, rollback changes in case it is needed.
store.subscribe("/users/:index", async (newUser, oldUser, restore) => {
const response = await api.users.update(newUser)
if (response.status >= 400) {
restore(oldUser).errors.push(response.message)
}
})
This would be very useful for implementing optimistic UIs, where local changes are applied right away and may be rolled back depending on the server's response.
On the example below, the first subscription logs out an invalid state for the store, where name
still says Diego
. The second subscription, though, logs out the state properly, which leads me to believe that the Tree#root
is not updated properly upon mutations.
const store = new Store({
user: {
name: "Diego",
age: 32,
}
})
store.user.name = "Bianca"
store.subscribe("/", state => console.log(state))
store.subscribe("/", state => console.log(state.user))
Collect some perf metrics and check for memory leaks.
https://www.toptal.com/nodejs/debugging-memory-leaks-node-js-applications
This would allow for creating a Store
with an initial async state, likely provided by an API (GraphQL??)
const promise = fetch("my.api.com/state")
const store = new Store(promise)
store.subscribe(console.log)
Currently, subscribers are only notified of mutations when they happen and never have a chance to handle the current store state upon their subscription.
This would allow for instance React dev tools to correctly show the current app state on its first rendering cycle (before any user interactions have taken place).
The current README file is outdated and needs to be updated to reflect the new Arbor API (subscriptions).
arbor-react
)arbor-timetravel
repoarbor-react-app
to the examples list)This would allow any component to be rendered and data would be accessed via props
rather than this.state
.
Every framework/lib is as good as the tools supporting its development. A devtools plugin for (initially) chrome would help to reduce the adoption friction of Arbor. Here are some ideas for features:
store.state.users[0].name
) the accessed path within the tree is highlighted.const store = new Store({
active: false,
})
store.state.active === undefined
// => true
This package would be a proof-of-concept project to validate whether or not arbor would provide a nice way to handle time travel functionality for any given app.
Basically, this package would allow one to travel back and forth in time and see all the state mutations that happened in that period.
Prototype:
import Store from "arbor"
import withTimetravel from "arbor-timetravel"
const StoreWithTimetravel = withTimetravel(Store)
const store = new StoreWithTimetravel({
todos: []
})
store.timeline.back(1)
store.timeline.forward(1)
store.timeline.last()
store.timeline.first()
Currently, subscribers are notified of any state change, receiving the new state tree root node as argument.
The idea is to allow subscriptions to created for specific mutation paths, e.g.:
// Subscribe to new posts being pushed into a user's posts list
store.subscribe("/users/:index/posts/push", (post) => {
// do something with new post
})
// Subscribe to new posts being pushed into a user's posts list
store.subscribe("/users/:index/name", (user) => {
// do something with user whose name was just updated
})
We might want to hold off on this feature until there are more clear use cases.
Props provided to the connected component are not forwarded to the wrapped component.
const ConnectedApp = connect(store)(App)
<ConnectedApp prop1="123" prop2="another prop" />
On the example above, neither prop1
nor prop2
is forwarded to App
.
This is totally not an arbor core concern. It should be provided as its own package since it is not code every app would require and can be optionally added as a separate dependency.
It would be quite convenient if we could easily remove branches of the state tree by simply calling node.$destroy()
.
If node
is a child of an object, the operation would be delete parent[nodeProp]
. In case the node is an array item, the operation can be fulfilled by Array.splice
.
Remove elements from arrays is a common operation in React apps which should make a case for this API. Removing props from objects seems a little less common, but could still be handy should the user decide to build normalized key/value stores.
Currently, every mutation operation (set, Array#push, etc...) will notify all subscribers as soon as they are done running. Because of that, if one needs to perform a more complex mutation, by changing multiple properties of a given node, for instance, that would cause subscribers to "see" the intermediary states required to complete the mutation. With transactions, mutations can be performed atomically:
const store = new Store({ user: { age: 32 }})
store.subscribe(console.log)
store.state.user.age++
// subscribers are notified
store.state.user.name = "Diego"
// subscribers are notified
Node#transaction
would allow multiple mutations to be applied to a given node in an atomic fashion, similar to DB transactions:
const store = new Store({ users: [{ age: 32 }]})
store.subscribe(console.log)
store.state.users[0].transaction(user => {
user.age++
user.name = "Diego"
console.log(user)
// => { name: "Diego", age: 33 }
})
// subscribers are notified only once
Mutations performed within the #transaction
closure happen "atomically" and subscribers will perceive the final mutation as if it were a single one. Additionally, within the transaction closure, mutations are visible right away (as shown in the example above).
A GIF is worth 10000 words...
Basically, a node bound to path /array/:index
must have its path refreshed when items removed/added to the array affect the path.
Example:
const todos = [
{ id: 1, ... }, // path = /todos/0
{ id: 2, ... }, // path = /todos/1
]
When removing todos[0]
, the path of the remaining todo, shoud no longer be /todos/1
but rather /todos/0
.
It would be very convenient if mutations could take promises in addition to sync values, example:
store.users.push(fetch("app.com/users/123"))
store.todos = fetch("app.com/todos?limit=20")
If the mutation value is a Promise
, arbor would then only apply the corresponding mutation upon promise resolution.
Failures can still be handled by the caller:
const userPromise = fetch("app.com/users/123").catch(console.log)
store.users.push(userPromise)
This is particularly useful when APIs reflect exactly the data structure expected by the front-end (BFF pattern).
Basically allow for:
store.subscribe(state => console.log(state))
rather than:
store.subscribe(Path.root, state => console.log(state))
This would allow a local store to connect with different remote data sources, such as a Rest API, GraphQL, or even another arbor store via a peer-to-peer connection.
store.connect(new MyRestApiAdapter)
http://graphql.org/graphql-js/running-an-express-graphql-server
store.connect(new MyGraphQLServerAdapter)
https://www.html5rocks.com/en/tutorials/websockets/basics
store.connect(new MyWebsocketChannel)
https://www.html5rocks.com/en/tutorials/webrtc/basics
store.connect(new MyWebRTCConnection)
store.connect(new LocalStorageAdapter)
Allow binding instance variables to prop fields. This could provide an even higher data abstraction level.
class App extends React.Component {
@prop // binds this.users to this.props.users
users
@prop("todo_list") // binds this.todos to this.props.todo_list
todos
render() {
// JSX...
}
}
MTree
provides a new state tree engine (extending the default PTree
) adding support to model abstractions for registered state tree path patterns.
This could be a separate, optional package since developers may choose to not use models (for simple apps it might not be needed...) thus, reducing the number of dependencies one would have to bundle within their app.
This would allow one to manipulate an array of proxied arbor nodes through a seamless Array API.
Setup:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.