GithubHelp home page GithubHelp logo

radonjs / radon Goto Github PK

View Code? Open in Web Editor NEW
77.0 5.0 6.0 282 KB

Object oriented state management solution for front-end development.

Home Page: http://radonjs.org

License: MIT License

JavaScript 100.00%
state state-management react object-oriented object-oriented-programming frontend-framework frontend-webdevelopment time-travel predictable predictable-state

radon's Introduction

Build Status npm

Radon is an object-oriented state management framework for JavaScript applications.

Read our documentation at radonjs.org

Why?

Data Encapsulation

One of the first goals of Radon was to implement an object oriented state manager capable of data encapsulation. Many state managers allow pieces of state to be accessible by any component or module, and with that access follows modification allowances. This inherently conflicts with a ubiquitous object oriented programming practice: limiting scope. Limiting the scope of a variable or method provides better context for its purpose and makes it easier to reason about. Plus, there's the added bonus of protecting data from being modified by script that has several degrees of separation. Many programming languages have native features to handle data encapsulation such as privatized class attributes and methods. Unfortunately, Javascript doesn't have the same privatization features held by universal languages such as Java and C/C++. Therefore, the data encapsulation feature of Radon needed to be derived by other means.

To understand encapsulation in Radon, it is first important to understand how the data is organized. Radon is built using a tree data structure. Pieces of state are stored in specially designed nodes and are organized in a way that parallels the component tree of many common frontend frameworks such as React or Vue. For example, if a developer created an initial App component that needed access to variables in state, a corresponding AppState node would be created that contained those specific variables and any accompanying modifier functions. Now let's say the App component renders two more components named Navbar and Main. If Navbar were to be stateful, it would need a corresponding node called NavbarState. If the same thing can be said for Main, then it would have a corresponding state node called MainState. If a frontend component is intended to be stateless, then there will be no corresponding state node. So now we can hopefully start to imagine that the App Component is at the top of a component tree (as the root), with NavbarState and MainState branching beneath it. The same can be said for the State Tree. AppState is our root, with NavbarState and MainState branching below.

But what does this mean for data encapsulation? The intention for the State Tree is for state nodes to share their data and modifiers with corresponding frontend components. However, this implementation alone would be too constricting of the data. Therefore, frontend components are not only able to access the data from their corresponding state nodes, but also the data from its parent, grandparent, and any further parent tracing back to the root. Now there's a greater sense of flow that encourages commonly used and shared data to be stored near the root, and specialized data to be stored as leaves on the tree. In sum, frontend components will have access to parental lineage data, but will not have access to their sibling’s or children's data. Thus, varying pieces of state are exposed where they are needed, and hidden where they are not.

Component Rendering Linked to Objects in State

Another feature of Radon intends to remove unnecessary re-rendering that can emerge from modifying objects in state. In other state management systems, modifying a single key/value pair in a plain object or an index in an array will result in a re-render of any component subscribed to the object. The Radon State Tree solves this problem by deconstructing objects into state nodes by index or key/value pairs. The object deconstruction feature allows for direct modification of these indices/pairs and triggers a re-render of only the component listening to that particular data point.

Asynchronous Modifications to State

Modifiers are functions written by the developer that can only modify a single state variable. Developers have the option to create an asynchronous modifier which may seem problematic if multiple modifiers are called in tandem to edit the same piece of state. However, Radon ensures that all state changes, whether asynchronous or synchronous, occur in the order of initial invocation. This is accomplished with an asynchronous queue that awaits the completion of the most recently invoked modifier before progressing to the next. Hence, the developer does not need to worry about conflicting state changes or out of order updates.

Getting Started

To install the stable version using npm as your package manager:

npm install --save radon-js

The Radon source code is transpiled to ES2015 to work in any modern browser. You don't need to use Babel or a module bundler to get started with Radon.

Most likely, you'll also need the React bindings and the developer tools.

npm install --save react-radon

Unlike Radon, React doesn't provide UMD builds, so you will need to use a CommonJS module bundler like Webpack, Parcel, or Rollup to utilize Radon with React.

How Radon Works

import { StateNode } from 'radon-js'

/*
StateNode is a class needed for creating instances of state. In Radon, StateNodes are created in
tandem with frontend components. The naming convention is important here; if you have created
a frontend component called App with the intent of statefulness, then an instance of StateNode must be 
declared and labeled as AppState. This will allow the App component to properly bind to AppState
at compile time.


The new instance of StateNode takes two arguments: the first argument is the name of the StateNode you
are creating which must follow our naming convention. The second argument is the name of the parent
node. One StateNode must be considered the root of the state tree. Therefore, at only one occasion can
the parent argument be omitted. This instance of StateNode will be considered the root. Every other
StateNode must take a parent argument.
*/

const AppState = new StateNode('AppState');
// or
// const AppState = new StateNode('AppState', 'OtherState');

/*
To declare variables in state, the method initializeState must be called which takes an object
as an argument. The variable names and their data should be listed in the object as key-value pairs.
*/

AppState.initializeState({
  name: 'Radon',
  status: true,
  arrayOfNames: []
})

/*
Modifiers are functions that modify a single variable in state. Modifiers are attached to variables by
calling the method initializeModifiers which also takes an object as an argument. The keys of the
argument object must correspond to variables that have already been declared in AppState. The values
are objects that contain the modifier functions as key-value pairs. There are two types of modifiers
in Radon. The first type, as seen below, can accept either 1 or 2 arguments. The 'current' argument
will automatically be injected with the bound state variable. The 'payload' argument is any data that 
can be used to modify or replace the 'current' value of state. Even if the current value of state is 
not used in the modifier, it will still be passed in automatically.
*/

AppState.initializeModifiers({
  name: {
    updateName: (current, payload) => {
      return payload;
    }
  },
  status: {
    toggleStatus: (current) => {
      return !current;
    }
  }
})

/*
It is important to note that when these modifiers are called from a component, only the payload argument
must be passed into the function as Radon will fill the 'current' parameter by default.
*/

<button onClick={() => this.props.name.updateName('Radon is cool!!!')}>Click Me</button>
<button onClick={() => this.props.status.toggleStatus()}>Click Me Too</button>

/*
The second modifier type is what helps Radon eliminate unnecessary re-rendering of frontend components.
This modifier type accepts three arguments and is used exclusively with objects. *Note that
initializeModifiers should only be called once. It is shown again here for demonstration purposes only*.
*/

AppState.initializeModifiers({
  arrayOfNames: {
    addNameToArray: (current, payload) => {
      current.push(payload);
      return current;
    },
    updateAName: (current, index, payload) => {
      return payload;
    }
  }
})

/*
The modifier addNumberToArray is nothing new. Since the goal of the modifier is to edit the array as a 
whole, the entire array object is passed into the 'current' parameter. A modifier that edits the array 
will cause a re-render of any component that subscribes to the array. However, we may have
circumstances in which we only want to edit a single index within an array. In this case we create a
modifier that accepts an index. The 'current' value will always reflect arrayOfNumbers[index]. This 
will prevent a re-render of components listening to the entire array, and will instead only re-render
components listening to the specified index.

Again, it is important to note that the 'current' parameter will be injected with state automatically.
*/

<button onClick={() => updateAName(0, 'Hannah')}>Edit an Index!</button>

/*
The same logic applies to plain objects. Instead of passing a numerical index into a modifier, the key 
of a key-value pair can be passed in instead.

Objects can be nested and it is possible to create modifiers for deeply nested objects. Ultimately, the
modifier will always be bound to the parent object. However, the key/index parameter will transform into 
a longer chain of 'addresses' to tell Radon exactly where the data is stored. For example:
*/

names: {
  first: ['', 'Beth', 'Lisa'],
  last: {
    birth: ['Mitchell', 'Sanchez', 'Delaney'],
    married: ['Mitchell', 'Smith', 'Delaney']
  }
}

/*
To inject the name 'Hannah' into index 0 of the 'first' array, the specified 'address' would be first_0.
To change the value of index 2 of the 'married' array, the specified 'address' would be last_married_2.
*/

/*
Once all StateNodes have been declared, they should be combined in the function combineStateNodes. The
returned object is known as the silo.
*/

import AppState from './appState';
import NarbarState from './navbarState';
import mainState from './mainState';

const silo = combineStateNodes(AppState, NavbarState, MainState);

Bind the state

In order to use the Silo state in a component, it must be passed to the same from the top of the application. That depends on the framework binding. Below you can find a working example of use of Radon on React via react-radon, the react binding for this library, as an example:

import {render} from 'react-dom';
import {Provider} from 'react-radon';

// Silo from Exported combineNodes from the example before
import silo from './localSiloLocation';

render(
  <Provider silo={silo}>
    <App />
  </Provider>,
  document.getElementById('root'));


// And in the component where you need the piece of state

import React from 'react';
import { bindToSilo } from 'react-radon'

const ReactComponent = (props) => {
  return (
    <div>
      {props.name}
    </div>
  )
}

export default bindToSilo(ReactComponent);

Built With

Rollup - Module Bundler

Babel - ES2015 transpiling

Versioning

2.0.0 We use SemVer for versioning.

Authors

Hannah Mitchell,

Hayden Fithyan,

Joshua Wright,

Nicholas Smith

License

This project is licensed under the MIT License - see the LICENSE.txt file for details

radon's People

Contributors

g0ldensp00n avatar hlmitchell avatar joshua-wright76 avatar ntsmitty avatar vikkio88 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

radon's Issues

More React/Vue practical examples?

Hi, amazing work in here, it looks really promising.
Just one small thing though, the documentation on the Readme is too verbose and it is, imho, missing an example of Component that uses a piece of state from a silo.
You can see how to trigger a mutation, but not how to pass that piece of state to the component, let's say the equivalent of connect() on Redux.

Silo

Silo

The Silo is used to store the state of the App. It is structured to be bound to the Component structure of the Application being implemented. This approach allows the Silo to maintain encapsulation of the data that it hold while still providing access to the data in a predictable way. The Silo is a collection of SiloNodes, each with their own set of subscribers and modifiers which act to update elements of the Silo in a predictable way.

SiloNode

A SiloNode is a structured element of the Silo. The structure of the Node allows for updates that propagate to all of the dependent subscribers, along with allowing the minimum amount of subscribers to be notified for each change.

SiloNode {
    value: {
       //ChildSiloNodes
    },
    modifiers: {
          modifier: //Code to Update Value
    },
    queue: [/* List of Modifiers To Be Implemented on Value*/],
    subscribers: [/* List of Components Subscribed to Node Updates */],
    parent: //ParentSiloNode
}

Modifiers

Modifiers act as the way to create predictable changes to the Silo in Radon. A modifier acts to perform some functionality before updating the value of a SiloNode and propagate the change to the Node's subscribers.

  function modifierName (current, payload) {
    return current + payload; //Changes the Node Value and Notifies Subscribers
  }

Object Modifiers

SiloNodes that own a set of predictable Objects, which all may need similar modifiers can implement a special modifier which acts on individual children that it controls. This modifier takes an extra parameter, a key which tells the modifier which child it should run the modifier on.

  function modifierName (current, key, payload) {
    /* currentValue is the Value of the Child Selected with the Key Parameter. */
    return current + payload; //Changes the Child Node Value and Notifies the Child's Subscribers
}

Silo Structuring

The Silo is structured into a tree as a means of providing encapsulation to state management. This tree structure binds to the Component Tree in order to pass props to each element that requires them. This connection between the State Tree and the Component Tree means that each Component will only get access to its parent's state, and the state that it holds, while being prevented from accessing the state of its sibling components.

Async Queue

The Async Queue is the way that Radon handles asynchronous changes to the Silo. When a modifier is run on a SiloNode that requires an asynchronous event, they are added to a queue. If another modifier seeks to update the SiloNode before the asynchronous change has updated the value and propagated the change, it will be added to the queue behind the asynchronous function. This allow SiloNode changes to happen asynchronously in a predictable manner.

React-Radon Binding

React-Radon Bindings (Repo)

The React Bindings for Radon allow for the State tree to be bound to the Component tree, passing down the necessary props to each component in React. This is contrary to other state libraries which require the developer to manually pull in the require props from the state.

Tree Binding

Binding the silo to components in React is achieved by wrapping the exported, or called class in a bind function which will perform the work or matching the component tree to the tree structure of the silo. These bound components will have props exposed to them matching the nodes in the silo.

import React from 'react';
import {bind} from 'react-radon';

class App extends React.Component {
   /* React Code */ 
}

export default bind(App);

Subscriber Relationship

Binding components automatically makes a subscriber bond between the silo and the component. Allowing individual components to re-render when their watched node has a modifier run on it. This relationship allows for re-rendering the smallest amount of components, without the need for deep-diffing optimizing the speed of Radon's silo.

Modifying the Silo

The Silo is modified through modifiers on the props that are attached to the components. Each prop assigned to the component has it's own set of modifiers passed down to it from the store. These modifiers act to update the piece of state that the component is tied to and are useful in reducing the scope of the re-render.

class App extends React.Component {
       render() {
             this.props.pieceOfSilo.modifierName(payload);
             return /* element to render */
       }
}

export default bind(App);

Getting State of Nodes with Nested Collections Don't Return Formatted Array

Describe the bug
Getting the State on a Container Node with nested state containing an array, or object returns an array or object of VirtualNodes, rather than the formatted object. Rather, it should return an array of the primitive values only, not VirutalNodes

To Reproduce
Steps to reproduce the behavior:

  1. Create A ConstructorNode, and Initialize the State with an Array Populated with Values
  2. CombineNodes with the ConstructorNode
  3. Called Get State on the SiloNode returned by combineNodes
  4. Notice that the array is returned as a set of virtual nodes rather than an array of values

Expected behavior
If I initialize state with an array, [1, 2] the array returned when calling get state should consist of [1, 2] rather than the current returned value of [VirtualNode {val: 1 ...}, VirtualNode {val: 2 ...}]. This should be true of all values in VirtualNodes that are not ContainerNodes.

Test Snippet

test('State Is Correctly Initialized', () => {
        let rootNode = new constructorNode('Root');

        rootNode.initializeState({
            testString: 'Testing',
            testNum: 123,
            testArr: ['1', '2', '3'],
            testObject: {
                key1: '1',
                key2: '2',
                key3: '3'
            }
        });

        let generatedTree = combineNodes(rootNode);

        expect(generatedTree.Root.getState().testString.val).toBe('Testing');
        expect(generatedTree.Root.getState().testNum.val).toBe(123);
        console.warn('State Is Correctly Initialized: Removed Due to Bug #56 - https://github.com/radonjs/Radon/issues/56');
        //Removed Due to Bug #56 - https://github.com/radonjs/Radon/issues/56
        //expect(generatedTree.Root.getState().testArr).toEqual(['1', '2', '3']);
        //expect(generatedTree.Root.getState().testObject).toEqual({key1: '1', key2: '2', key3: '3'});
    });

Current Value Passed into the Modifier Isn't Correct Datatype

Describe the bug
Trying to modify an array with a modifier doesn't work as the current value that is passed into the array is an object, and not an array.

siloNode.initializedModifiers({
     arrayNode: {
            addToArray(current) {
                 current.push('test'); //Doesn't Work Because Current is An Object.
            }
     }
}

To Reproduce
Steps to reproduce the behavior:

  1. Create an Array in the Silo.
  2. Create A Modifier to Modify the Array
  3. Running that Function and Observe the Object that came in.

Expected behavior
The current value passed into the modifier of the array function should be an array, rather than an object. As this is confusing to the developer who would be expecting an array to come in.

Get State

getState()

The getState method should return the variables and modifiers available to a component which will include parent data.

notifySubs()

The notifySubs() method should inform the subscribers that the data it is watching has been changed. All the subscribers are the render functions of the components that are subscribing to data. Whenever the data changes, (whenever a modifier is called) notifySubs() is called, and the components are all rerendered.

Virtual Silo

Virtual Silo

When Radon gets part of the silo to pass down to render functions, it can only return chunks of the state as its stored inside Radon, as a tree of silo Nodes. This means that when the developer gets the data back, it's in a format that is unrecognizable when compared to the data that they originally put into the silo with initializeState(). For this reason, Radon should also internally maintain something called a Virtual Silo.

The Virtual Silo

The virtual silo is the developer-facing representation of the silo. While Radon needs to maintain two-way bindings in the silo (upward to parents and downward to children) the developer should only be able to access the state attached to a given node or its parents, which is why the virtual silo is a bottom-up tree.

The virtual silo will be tied to the Radon silo. Anytime a modifier changes parts of the silo, those changes will be represented in the virtual silo before they are sent to silo Node subscribers.

Example

If a state node is initialized like so:

StateNode.initializeState({
    lyric: "its the eye of the tiger its the thrill of the fight"
})

//A new node is initialized in the state, with the name "lyric" and the value 
set to the string "its the eye of the tiger its the thrill of the fight"

StateNode.initializeModifiers({
    lyric: {
        append: (payload, previous, updateTo) => {
                updateTo(previous + payload);
            }
    }
})

//A modifier is attached to lyric called "append" which takes a new string,
//then updates the value to the old string with the new string appended to the end 

silo.subscribe(render, 'lyric')

function render(data){
    //without a virtual silo, the lyric node would come into this function like so:
    data.val; //"its the eye of the tiger its the thrill of the fight"
    data.modifiers.append(", rising up to the challenge of our rivals")

    //With a virtual silo, the data isn't wrapped like a silo node when it is pushed into render functions
    data.val; //"its the eye of the tiger its the thrill of the fight"
    data.append(", rising up to the challenge of our rivals")
}

//Using a virtual silo would make development using Radon more intuitive and useful to the developer.
//Because the data comes in a recognizable format, they can interface with it as they would an object.

Subscription to Children isn't Automatic

Describe the bug
Methods that update the state owned by a parent, don't actually trigger the parent to be re-render. Meaning that if the parent owns any primitive data types, they won't cause re-rendering when the are updated.

To Reproduce
Steps to reproduce the behavior:

  1. Create a State with a Primitive Data Type
  2. Create a Modifier that Updates the Primitive Data.
  3. Setup Components, and Run the Modifier
  4. See the No Re-render happens.

Expected behavior
The parent should auto subscribe to primitive data type changes, without forcing the developer to manually subscribe to the changes.

Modifiers Aren't Pulled Into Parent's Modifiers

Describe the bug
When creating modifiers with Radon, each modifier should be pulled out and placed into a the modifiers object of the parent, in the same structure as the data in the parent object. As the data in the parent should be deconstructed and back as it's primitive form.

To Reproduce
Steps to reproduce the behavior:

  1. Create A ConstructorNode called Root, and Initialize the Modifiers with Functions
  2. Create a variable generatedTree and set it to CombineNodes with the ConstructorNode
  3. View generatedTree.Root.modifiers and notice that it isn't filled with the created modifiers.

Expected behavior
The data returned to the developer should be deconstructed from SiloNodes, meaning that it should have a .val that has to be called, along with the modifier should be pulled out and kept in the ContainerNode, as the developer is expecting to work with a state that mirror's the way they setup the state tree.

Test Snippet

    test('Collections Should Be Given KeySubscribe', () => {
        let rootNode = new ConstructorNode('Root');

        rootNode.initializeState({
            testArr: [1, 2, 3],
            testObj: {key1: 1, key2: 2, key3: 3}
        });

        let generatedTree = combineNodes(rootNode);

        console.warn('Collections Should Be Given KeySubscribe: Removed Due to Bug Bug #57 - https://github.com/radonjs/Radon/issues/57');
        //Removed Due to Bug Bug #57 - https://github.com/radonjs/Radon/issues/57
        // expect(generatedTree.Root.modifiers.testArr.keySubscribe).toBeTruthy();
        // expect(generatedTree.Root.modifiers.testObj.keySubscribe).toBeTruthy();
    });

Setup Testing with Jest

Setup Jest

  • Create a Jest Test Folder to hold all of the individual testing files.

Testing Silo

  • Author Test that ensure that the function from the Silo behave as expected.

Modifiers on Objects in Radon

Object Modifiers

Currently any modifier attached to an object parent in the silo is only capable of being called on the children of the parent. An option needs to be created to allow modifiers to be called on the object itself. We currently have the following syntax for adding object modifiers:

AppState.initializeModifiers({
  object: {
    changeValueInObject: (current, index, payload) => {
        // returns a change to a value in an object
    }
  }
})

However, an object parent must also be able to accept the following modifier:

AppState.initializeModifiers({
  object: {
    changeObject: (current, payload) => {
        // returns a change to the entire object
    }
  }
})

The combineNodes function must know to tell the difference between modifiers.

Creating Nodes at Runtime

Currently the silo deconstructs objects and creates nodes for every primitive value. The silo must be able to create new nodes at runtime if a value is added to an object.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.