metamatic

2.5.7 • Public • Published

Metamatic® State Manager

A state management library for JavaScript-based web-apps.

Chapters

Introduction

Metamatic® framework is a simple and clutter-free state manager for JavaScript apps and for developers who want to get things done. Metamatic® provides a robust toolset for data communication between components inside your browser-based UI software. It can be used together with any modern JavaScript UI framework such as Vue, React, Angular and even basic JavaScript apps without any specific frameworks.

water

The Concept

Metamatic® framework solves a fundamental problem in frontend software design: when any data is changed anywhere in the application, this change must be reliably radiated to all parts of the software that uses that data. For example, your frontend app has many components that display user's order history. When that data changes it should be consistently updated in all parts of the app.

When you think about Metamatic®, think about throwing an ice cube into a glass full of water.

  • Think about the glass. It is the data store, the state container.
  • Think about the water. It is the data that splashes into every direction from the glass.
  • Think about the ice cube. It is the direct function invocation you call to update the data container.

Metamatic® provides an easy way to manage your data stores and states inside them. In Metamatic®, the data flows typically as follows:

  1. A component updates the store by directly invoking an updater function that is defined in a store utility file.
  2. The store updater function in the store utility file updates the store and fires an update event containing a copy of the store itself.
  3. The component/components connected to the store receive the event from the store.
  4. The connected components update their state from the event and the component refreshes itself.
  • The data maintains its integrity since updating the master copy in the central data store radiates the change to every place where needed!
  • The data store always fires only copies of itself, therefore there's no way to sneakily mutate the master copy.
  • Events are almost the same as stores since updating a data store fires an event with the same name as the store itself. A copy of the store is the passenger of the event.

<- Back to contents

Persistent States

Being tired of web portals that forget their state or get strangely messed when the browser is refreshed? Metamatic® solves this problem by offering various out-of-the-box persistency modes, localStorage, sessionStorage and memoryStorage. Meaning, the web site remains exactly in the same state as it was before you refreshed the browser.

<- Back to contents

Say Goodbye to Manual Switch-Cases

Metamatic® has fundamental differences to some well-known state manager frameworks. Metamatic® directly binds event handlers to corresponding events already in the very moment you define them by calling handleEvent or connectToStore function. When the listener functions inside components are already inherently connected to their data source, you don't need to explicitly write clumpy switch-case structures to explain the application what action shall be invoked upon which event. Simplistic and clean code prevents the application from turning into buggy bubble gum that is too expensive to maintain.

Metamatic® frees you from the need to manually write switch-case structures since it connects states to their listeners silently using hash tables, taking internally advantage of JavaScript's` associative arrays. With this solution, you don't need to manually connect events to their handlers anymore.

<- Back to contents

Clean Solution without Provider Clutter

One major difference to verbose state manager frameworks is that you don't really need to "pre-configure" your app to use Metamatic®. You don't need to wrap your application inside obscure "Provider" wrappers and you don't need to "inject stores" and other structures to your classes to enable a state container. Any class, component, object or helper function can be connected to Metamatic® at any point of the project without any need to do major refactoring to existing application logic or code structure. You can use functions provided by Metamatic® on the fly anywhere inside your app, any time. If your application already uses some other state container framework, you can still introduce Metamatic® into your app without removing or changing anything that already exists.

<- Back to contents

Robust State-Based Solution Without Props-Hassle

A major innovation within Metamatic® framework is that it eliminates the props vs. states dilemma that most state container frameworks seem to have. In Metamatic® framework, your components are not directly connected to states inside global stores. Instead, Metamatic® effectively copies global states into component's local states. This gives you more freedom to decide which states you want to keep as component's local states and which ones you want to connect to global states. In Metamatic®, the root states are called stores. Stores can have nested properties, which are all understood as nested states. You can connect any component to listen to entire stores as well as just one nested state deep inside a store.

<- Back to contents

Source Code and Examples

Sources

Metamatic® is available as installable package at Npmjs.com.
You can also explore the source code at GitHub. Or visit the official home page at www.metamatic.net.

<- Back to contents

Examples

Check out the source code of Car App demo for a practical example of Metamatic® framework in action, and the actual deployment of the demo live!

Also checkout the Router Demo which also demonstrates the Router feature of Metamatic.

<- Back to contents

Blog

Check out the Metamatic® blog for articles about using the framework!

<- Back to contents

Getting Started

Installing Metamatic®

To install Metamatic® in your project, type:

npm i --save metamatic

Selecting Persistency Strategy

In your app's starting point, for instance Main.js or App.js file, configure Metamatic® to use any of the three available persistency modes calling

useLocalStorage(), useSessionStorage() or useMemoryStorage()

from which localStorage is set on by default.

<- Back to contents

Stores

Define Your Stores as Constants

A good practice is to write constants for all your stores in the app. Using constants limits the risk of mistyping names:

export const STORE_USER_INFO = 'STORE_USER_INFO';
export const STORE_CAR_OPTIONS = 'STORE_CAR_OPTIONS';

A good place to define store constants is inside your store utility files that update those stores anyway.

<- Back to contents

Initializing Stores

Initializing one store can be done with initStore function. initStore is practical because it won't overwrite any existing states inside a store if the store already exists

import {initStore} from 'metamatic';
 
initStore(STORE_USER_INFO, {
  username: 'Some new userName'
});

Initializing many Metamatic® stores simultaneously with initStores:

import {initStores} from 'metamatic';
 
initStores({
  [STORE_USER_INFO]: {
    username: 'Some new userName'
  },
  [STORE_CAR_OPTIONS]: {
    options: [
      {
        key: 'tesla',
        label: 'Tesla'
      },
      {
         key: 'toyota',
         label: 'Toyota'
      }
    ]
  }
});

Or if you don't want too many stores, set many states under one store:

initStore(STORE_USER_DATA, {
  username: 'Jon Doe',
  carOptions: [
    {
      key: 'tesla',
      label: 'Tesla'
    },
    {
      key: 'toyota',
      label: 'Toyota'
    }
  ],
  address: {
    streetAddress: 'Some street 1'
  }
});

<- Back to contents

Retrieving Data from Stores

When you want to retrieve an entire store from Metamatic® state manager, just simply use getStore function:

import {getStore} from 'metamatic';
 
const userStore = getStore(STORE_USER_DATA);

But you may just need one particular state inside a store. For retrieving a nested state inside a store, use getState function:

import {getState} from 'metamatic';
 
const streetAddress = getState(STORE_USER_DATA, 'address.streetAddress');

But as in Metamatic®, a root state being called store and that store in turn being just a simple associative array, you can actually invoke getState without a second parameter. In such case, Metamatic® will return the root state, the store:

import {getState} from 'metamatic';
 
const userDataStore = getState(STORE_USER_DATA);

Remember that getters always return a copy of the store. You can safely modify the received object without mutating the master copy inside the store!

<- Back to contents

Updating Stores

When updating stores with updateStore or updateStores function, the states inside an existing store or stores are merged with the new incoming object. Those values that are not defined in updater object will remain untouched in the store.

import {updateStore} from 'metamatic';
 
updateStore(STORE_USER_INFO, {
  username: 'Some new userName'
});

The example above will overwrite or set 'username' state but lets streetAddress remain as is. updateStore returns the new combined object:

const mergedObject = updateStore(STORE_USER_INFO, {
  username: 'Some new userName'
});

To update many stores simultaneously:

import {updateStores} from 'metamatic';
 
updateStores({
   [STORE_USER_INFO]: {
     address:  {
      streetAddress: 'Jon Doe Street 3',
      city: {
        zipCode: '00100',
        name: 'Helsinki'
      }  
     }
   },
   [STORE_CAR_OPTIONS]: {
     options: [
       {
         key: 'tesla',
         label: 'Tesla',
         active: true
       },
       {
          key: 'toyota',
          label: 'Toyota'
       }
     ]
   }
 })

You might want to update just a single state inside a store. For that, use setState function:

import {setState} from 'metamatic';
 
setState(STORE_USER_INFO, 'address.city.name', 'Malasiqui');

<- Back to contents

Rewriting and Clearing Stores

Functions setStore and setStores work similarly to updateStore and updateStores except they completely overwrite the store or stores with the new one. clearStore empties a store. Function existsStore can be used if a store exists.

<- Back to contents

Connecting React Components to Stores

Connecting a React component to listen for an entire store can be done with connectToStore and connectToStores. In ReactJS, use componentDidMount life cycle callback to connect your component to Metamatic® stores, for example:

export class SomeReactComponent extends Component {
  
  constructor(props) {
    super(props);
    this.state = {};
  }
  
  componentDidMount = () => connectToStores(this, {
    [STORE_USER_INFO]: (userInfo) => this.setState({
      ...this.state,
      userInfo
    }),
    [STORE_CAR_INFO]: (carInfo) => this.setState({
      ...this.state,
      myCarInfo: carInfo
    })
  });
}

The example above merges two entire stores with React component's existing state, STORE_USER_INFO as local nested state userInfo and STORE_CAR_INFO as nested state myCarInfo.

To only connect to one store:

componentDidMount = () => connectToStore(this, STORE_CAR_INFO, 
  (carInfo) => this.setState({
    ...this.state,
    myCarInfo: carInfo
});

There is a caveat, however! Every time either STORE_USER_INFO or STORE_CAR_INFO states are changed it will cause the component to be re-rendered even if there was nothing relevant in those stores for this given component. In such case, you don't want unnecessary component updates to happen. For this, use connectToStates to rather connect the listener component to a particular nested substate or substates inside a store:

componentDidMount = () => connectToStates(this, STORE_USER_INFO, {
  'address.streetAddress': (streetAddress) => this.setState({
    ...this.state,
    streetAddress
  }),
  'orderHistory.latestOrder': (latestOrder) => this.setState({
    ...this.state,
    latestOrder
  })}
)

In the example above, component is connected to two different nested states inside STORE_USER_INFO. Only a change in a nested state streetAdress inside address state and latestOrder change in orderHistory state will cause the component to update through its setState native function call.

Also remember here that all states and stores received this way are only clones of the master copy that resides protected inside Metamatic® state manager, thus modifying them locally won't mutate the master copy in the store.

<- Back to contents

Disconnecting Components from Metamatic® Stores

Disconnecting a component from MetaStore upon unmounting:

disconnectFromStores(this);

To call disconnect inside componentWillUnmount React life cycle function:

componentWillUnmount = () => disconnectFromStores(this);

<- Back to contents

Events

Even though events are typically connected to stores the way that updating a store causes Metamatic® to broadcast (or dispatch or radiate) a similarly named event as the store itself, there are situations that you want to fire a standalone event without updating any store. You may also want to implement a standalone event listener that handles events but does not necessarily update any store.

<- Back to contents

Implementing Event Listeners

When you want to handle events inside components that don't need to be unmounted or any static methods and utility functions, simply use handleEvent and handleEvents functions for registering handlers for vevents:

import {handleEvent} from 'metamatic';
 
handleEvent('MY-EVENT', (item) => {
  console.log('I process the event here..');
  console.log(item);
});

and many events:

import {handleEvents} from 'metamatic';
 
handleEvents({
  'MY-EVENT': (item) => {
    console.log('I process the event here..');
    console.log(item);
  },
  'OTHER-EVENT': (item) => {
    console.log('I process another one here..');
    console.log(item);
  },
});

<- Back to contents

Broadcasting Events

Similarly, broadcast an event into app-wide bit-space to be processed with all event handlers, use broadcastEvent function:

import {broadcastEvent} from 'metamatic';
 
const someObject = {something: 'here'}
 
broadcastEvent('SOME-EVENT', someObject);

broadcastEvent will dispatch a clone of the data sent as a parameter, therefore the receiver can't directly modify the source version.

Parameterless Events

broadcastEvent function does not need a parameter. It is correct to broadcast events without a passenger:

broadcastEvent('SOME-EVENT');

The example above will just pass a null value as parameter. Accordingly, you can implement a handler without parameter:

handleEvent('SOME-EVENT', () => {
  console.log('I process the event here..');
});

<- Back to contents

The System Event CONNECT

A very useful thing to know about Metamatic® is that every time a component is connected to a store or state, Metamatic® automatically fires a system event to inform anybody who listens that a component has been connected to a store or a state.

Consider that you connect a React component to a store such as:

componentDidMount = () => connectToStore(this, 
  STORE_USER_INFO, 
  (incomingStore) => this.setState({
    ...this.state, 
    userData: incomingStore.userData
  })
);

In the code snippet above, you want to connect your component to a store with name STORE_USER_INFO, and when the store is updated, it will be dispatched to this component. From the incoming store, userData state will be taken and put into this component's local state.

Now, what will happen if the user data is not available in the store? Absolutely nothing! But that is possibly a situation that you don't want. Therefore it is possible to make a store to listen for component connecting events and program them to act upon them.

When the component was connected to STORE_USER_INFO, Metamatic® hiddenly fired a CONNECT system event, which has syntax CONNECT/YOUR_STORE_NAME - that would be in this example "CONNECT/STORE_USER_INFO".

This is very helpful because you can add a piece of code to the user info store to handle such connect event:

const CONNECT_USER_INFO = 'CONNECT/' + STORE_USER_INFO;
 
handleEvent(CONNECT_USER_INFO, (listener) => optionallyLoadUserData());  //you can have empty params () if you don't need the listener - actually you should not need it.

The handler above is invoked every time when a component is connected to STORE_USER_INFO store.

The implementation for optionallyLoadUserData would check if the store already contains the user data and if not, then load it:

import {containsState, updateStore} from 'metamatic';
 
const optionallyLoadUserData = () => 
  !containsState(STORE_USER_INFO, 'userData') && loadUserData(response => 
  updateStore(STORE_USER_INFO, {
    'userData': response.data
  })
); 

The code example checks if STORE_USER_INFO contains state userData. If not, it invokes loadUserData function that actually loads the data from server - and finally updates the store, setting userData state that was received. updateState will cause the listener component actually to receive the user data in question. Function loadUserData can be implemented using any available Ajax library.

Read more about using CONNECT feature here!

<- Back to contents

Miscellaneous

License

Apache 2.0

Author and Copyright

Heikki Kupiainen / metamatic.net

Background

Metamatic® is based on earlier prototype Synchronous Dispatcher package but has many more improvements and is more suitable to be used together with ReactJS framework.

Metamatic® Based Router

Also check the Metamatic-based router library,
which is a simple router implementation based on the coreframework.

Read More

<- Back to contents

Dependencies (1)

Dev Dependencies (13)

Package Sidebar

Install

npm i metamatic

Weekly Downloads

0

Version

2.5.7

License

Apache-2.0

Unpacked Size

32.5 kB

Total Files

3

Last publish

Collaborators

  • oppikone