Skip to content

A scalable atomic state based state management framework designed for React, but can work with any framework with a render function.

License

Notifications You must be signed in to change notification settings

craigthings/atomx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

66 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

βš› AtomX (Alpha) - Predictable and Scalable Atomic State

Currently in Alpha stage of development. We are looking for feedback and pull requests to make this better state management for more people.

AtomX is a scalable atomic state based state management framework designed for React, but can work with any framework with a render function.

Core principles:

  • Scalable - API simple enough for prototyping, explicit enough for large scale apps.
  • Familiar - Built to allow syntax that is similar to other workflows and frameworks.
  • Predictable - All changes to state are explicitly subscribed to, and directly changed.
  • Flexible - Atomic state can be built into many different programming patterns and workflows.
  • Batteries Included - Fully supports TypeScript and JavaScript, and includes many components and APIs to make managing atomic state easier in large scale apps.

Core components:

  • State - A single atomic value that has a getter, setter, and is an event dispatcher.
  • Store - A container for multiple atomic states. It has a getter, setter, and is an event dispatcher.
  • Collection - A list-like structure containing atomic states, which also has a getter, setter, and is an event dispatcher.
  • Subscribing - All AtomX structures can be subscribed to, and have event listeners.
  • Computed State - State that's value is derived from changes to other state.

Installing

Platform independent library:

npm install atomx-state

React based library:

npm install atomx-state-react

Examples

Using Classes

Component State:

import { state, Subscriber } from "atomx-state";

// extend our component to support subscribing to atomic state
class CountExample extends Subscriber(React.Component) { 
  // create initial atomic state
  name = state('My Counter');
  count = state(0); 
  
  increment = () => this.count.set(this.count.get() + 1); // action to add 1 to state

  render() {
    this.subscribe(this.count, this.name); // subscribe to the state changes
    // get and save the current value into variables
    let count = this.count.get(); 
    let name = this.name.get();
    return (
        <p>
          {name} Clicked: {count} times.
          <button onClick={this.increment}>+</button>
          <input 
            type="text" 
            value={ name.get() } 
            onChange={ (e) => name.set(e.target.value) }
          >
        </p>
    );
  }
}
TypeScript Example
import { state, Subscriber } from "atomx-state";

// extend our component to support subscribing to atomic state
class CountExample extends Subscriber(React.Component) { 
  // create initial atomic state
  name = state<string>('My Counter');
  count = state<number>(0); 
  
  increment = () => this.count.set(this.count.get() + 1); // action to add 1 to state

  render() {
    this.subscribe(this.count, this.name); // subscribe to the state changes
    // get and save the current value into variables
    let count: number = this.count.get(); 
    let name: string = this.name.get();
    return (
        <p>
          {name} Clicked: {count} times.
          <button onClick={this.increment}>+</button>
          <input 
            type="text" 
            value={ name.get() } 
            onChange={ (e) => name.set(e.target.value) }
          >
        </p>
    );
  }
}

Global State:

import { Store, state } from "atomx-state";

class CounterStore extends Store {
  count = state(0);
  name = state('Unnamed Counter');
  
  constructor() {
    super();
    this.init(); // initialize this store
  }

  increment = () => {
    this.count.set(this.count.get() + 1);
  }

  setName = (name) => {
    this.name.set(name);
  }
}

export default new CounterStore();
TypeScript Example
import { Store, state } from "atomx-state";

class CounterStore extends Store {
  count = state<number>(0);
  name = state<string>('Unnamed Counter');
  
  constructor() {
    super();
    this.init(); // initialize this store
  }

  increment = () => {
    this.count.set(this.count.get() + 1);
  }

  setName = (name) => {
    this.name.set(name);
  }
}

Using Functional Components

Component State:

import { state } from "atomx-state";
import { subscribe } from "atomx-state-react";
// create initial atomic state.
let count = state(0); 
let name = state('My Counter');

function CountExample {
  
  let increment = () => count.set(count.get() + 1); // action to add 1 to state
  subscribe(count, name); // subscribe to the state changes

  render() {
    return (
        <p>
          {name.get()} Clicked: {count.get()} times.
          <button onClick={this.increment}>+</button>
          <input 
            type="text" 
            value={ name.get() } 
            onChange={ (e) => name.set(e.target.value) }
          >
        </p>
    );
  }
}
TypeScript Example
import { state, subscribe } from "atomx-state";
// create initial atomic state.
let count = state<number>(0);
let name = state<string>('My Counter');

function CountExample {
  
  
  let increment = () => count.set(count.get() + 1); // action to add 1 to state.
  subscribe(count, name); // subscribe to the state changes

  render() {
    return (
        <p>
          {name.get()} Clicked: {count.get()} times.
          <button onClick={this.increment}>+</button>
          <input 
            type="text" 
            value={ name.get() } 
            onChange={ (e) => name.set(e.target.value) }
          >
        </p>
    );
  }
}

Future features...

  • Time travel
  • Debugger
  • Rehydration

Working Online Examples

Classes (React):

Functional (React):

AtomX API

βš› State

"State" is a piece of atomic state. It can be get, set, subscribed to, listened to (using on or off), and dispatched from. Sometimes you need your app to react to a specific event, and not just change based on what state you've subscribed to.

state(value: any)

// Creating new atomic state

// JavaScript
count = state(0);

// TypeScript
count = state<number>(0);

Create a new instance of state;

State.get()

count.get(); // 0

Returns the value of the state.

State.set(state: Object)

Store.set();

Sets the value of the state.

State.reset()

count.reset();

Resets the value in the state to the initial value, even if the original value is undefined.

State.on(eventName: string, payload: any)

count.on(StoreEvents.CHANGED, changeHandler);

function changeHandler(storeReferece){
    console.log('Store changed!')
}

Add an event listener to your state. Only event currently dispatched is StoreEvents.CHANGED when the state value is changed.

State.off(eventName: string, handlerFunction: Function)

count.off(StateEvents.CHANGED, changeHandler);

Remove an event listener from your state.

State.dispatch(eventName: string, payload: any)

count.dispatch('INCREMENT', count.get());

// Somewhere else in your app

count.on('INCREMENT', countIncrementHandler);

Dispatch an event from your state.

πŸ”’ Store

A store is a way to contain and manage multiple atomic states and actions on those states. A store, when initialized, subscribes to all state within it. It is also an event dispatcher, and has some other convenience features. You could build your own version of this using the principles above of atomic state, but this is our "batteries included" API for storing all your state.

The examples below assume this global state matches the counter example found near the beginning of the document.

Store.get()

Store.get(); // { count: 0 };

Transforms all atomic states within the store into a flat object.

Store.set(state: Object)

Store.set({
  count: Store.count.get() + 1
});

Set any matching atomic state using and object.

Store.subscribe(changeHandler: Function)

Store.subscribe(storeChangeHandler);

function storeChangeHandler(storeReference){
  console.log('Store changed!')
}

Subscribe a handler to any changes to the store. The handler will be called when any state within the store changes.

Store.reset()

Store.reset();

Resets all state found within the store to their original values.

Store.on(eventName: string, payload: any)

Store.on(StoreEvents.CHANGED, changeHandler);

function changeHandler(storeReferece){
    console.log('Store changed!')
}

Add an event listener to your store. Only event currently dispatched is StoreEvents.CHANGED when any atomic state within the store is changed.

Store.off(eventName: string, handlerFunction: Function)

Store.off(StoreEvents.CHANGED, changeHandler);)

Remove an event listener to your store.

Store.dispatch(eventName: string, payload: any)

UserStore.dispatch('LOGIN_SUCCESSFUL', userData);

// Somewhere else in your app

UserStore.on('LOGIN_SUCCESSFUL', userLoginHandler);

Dispatch an event from your store.

Store.init()

Store.get(); // { count: 0 };

Usually called in the constructor of a new store. This initializes the store, making sure that all state in the store is subscribed to.

πŸ“š Collection

A collection stores a list of states. State can be added, removed, retrieved, and changed.

Example:

import { Store, state } from "atomx-state";

class TodoItem extends Store {
  name = state('');
  completed = state(false);
  
  constructor(initialName) {
    super();
    if(initialName) this.name.set(initialName);
    this.init(); // initialize this store
  }

  done = () => {
    this.completed.set(true);
  }
}

export class Todo extends Store {
  todos = collection();

  constructor() {
    super();
    this.init(); // initialize this store
  }

  addTodo = (name: String) => {
    this.counters.add(new TodoItem(name));
  };
}
TypeScript Example
import { Store, state } from "atomx-state";

class TodoItem extends Store {
name = state<string>('');
completed = state<boolean>(false);

constructor(initialName: boolean) {
  super();
  if(initialName) this.name.set(initialName);
  this.init(); // initialize this store
}

done = () => {
  this.completed.set(true);
}
}

export class Todo extends Store {
todos = collection<TodoItem>();

constructor() {
  super();
  this.init(); // initialize this store
}

addTodo = (name: String) => {
  this.counters.add(new TodoItem(name));
};
}

collection(value: any)

// Creating new atomic state

// JavaScript
count = collection();

// TypeScript
count = collection<TodoStore>();

Create a new instance of a collection;

Collection.get()

collection.get(); // [ counter1, counter2, counter3, ...];

Returns all state within the collection as an array.

Collection.set(state: Object)

collection.set([counter1, counter2, counter3]);

Replaces the collection with a new array of values.

Collection.subscribe(callback: Function)

collection.subscribe(storeChangeHandler);

function collectionChangeHandler(collectionReference){
  console.log('Store changed!')
}

Subscribe a handler to the collection that will be called when an item is added, removed, or changed.

Collection.filter( filterFunction: (value: CollectionType, index: number, array: CollectionType[]) )

let completed = collection.filter( todo => todo.isCompleted.get() === true );

Returns a filtered array of results where the filter function returns true.

Collection.reset()

collection.reset();

Resets the collection to its initial state, even if that's empty.

Collection.on(eventName: string, payload: any)

collection.on(StoreEvents.ADDED, addHandler);

function addHandler(collectionReferece){
    console.log('Item added!')
}

Add an event listener to your store. Available collection events to dispatch are:

CollectionEvents.CHANGED

CollectionEvents.ADDED

CollectionEvents.REMOVED

Collection.off(eventName: string, handlerFunction: Function)

collection.off(StoreEvents.ADDED, addHandler);

Remove an event listener to your store.

Collection.dispatch(eventName: string, payload: any)

collection.dispatch('LOAD_COMPLETE', loadHandler);

// Somewhere else in your app

collection.on('LOAD_COMPLETE', loadHandler);

Dispatch an event from your collection.

πŸ“š Computed State

The value of Computed State updates when any state within a referenced store changes.

Example:

import { Store, state } from "atomx-state";

class CounterStore extends Store {
  count = state(0);
  countPlusTen = computed(this, () => {
    return this.count.get() + 10;
  });
  
  constructor(initialValue) {
    super();
    this.init(); // initialize this store
  }

  increment = () => {
    this.count.set(this.count.get() + 1);
  }
}

// Elsewhere in your app...

countPlusTen.get(); // 10
CountPlusTen.on(StateEvents.CHANGE, (stateReference) => {
  stateReference.get(); // 10;
})

collection(storeReference: Object | any, computeFunction: function)

countPlusTen = computed(MyStore, () => {
  return MyStore.count.get() + 10;
});

Create a new instance of computed state. This often exists inside a global store.

Computed.get()

Returns the value of the computed state.

Computed.subscribe(callback: Function)

collection.subscribe(computedChangeHandler);

function computedChangeHandler(stateReference){
  console.log('Computed changed!')
}

Subscribe a function to any changes in the computed state.

Computed.on(eventName: string, payload: any)

collection.on(StoreEvents.ADDED, addHandler);

function addHandler(collectionReferece){
    console.log('Item added!')
}

Add an event listener to your store. The available collection event to dispatch is:

CollectionEvents.CHANGED

Collection.off(eventName: string, handlerFunction: Function)

collection.off(CollectionEvents.CHANGED, changeHandler);

Remove an event listener from your computed state.

πŸ“¬ Subscribing

Subscriber Component

Subscriber(Component: Class)

You can turn your component into a Subscriber so that it's easier to manage subscribing to state.

Using a different framework's component? Make sure it contains either a .forceUpdate method or .render method.

this.subscribe(stateReference: State | Store | Collection | Computed, stateReference, ...)

// In Class scope
this.subscribe(count, title);

Subscribe to a single or multiple states. If either are changed, your component will be updated.

Example:

import myGlobalStore from './stores/myGlobalStore';

class CountExample extends Subscriber(React.Component) {
  count = state<number>(0);

  componentWillUnmount = () => {
    this.unsubscribeAll(); // Unsubscribe from all state you've subscribed to before unmounting.
  }

  render() {
    this.subscribe(count, myGlobalStore); // Subscribe to your state.
    return (
      <div>
          <h1>{myGlobalStore.title.get()}</h1>
          Clicked: {this.count.get()} times {' '}
          <button onClick={this.increment}>+</button>{' '}
      </div>
    );
  }
}

Subscribe Hook

subscribe(stateReference: State | Store | Collection | Computed, stateReference, ...)

You can subscribe to state in your functional React component using the custom subscribe hook.

// In Function scope
subscribe(count, title);

Subscribe to a single or multiple states. If either are changed, your component will be updated.

Example:

import myGlobalStore from './stores/myGlobalStore';

let count = state<number>(0);

function Count() {
  subscribe(count, myGlobalStore); // Subscribe to the state using the subscribe hook.

  function increment() {
    count.set(count.get() + 1);
  }

  return (
    <div>
        <h1>{myGlobalStore.title.get()}</h1>
        <p>Clicked: {count.get()} times {' '}<p>
        <button onClick={increment}>+</button>{' '}
    </div>
  );
}

πŸ€– Testing

test('counter', t => {
  let store = new CounterStore();
  t.assert(store.count.get() === 0);

  store.increment();
  t.assert(store.count.get() === 1);

  t.end();
})

About

A scalable atomic state based state management framework designed for React, but can work with any framework with a render function.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published