Skip to content

Latest commit

 

History

History
698 lines (522 loc) · 20.7 KB

README.md

File metadata and controls

698 lines (522 loc) · 20.7 KB

OmniAural

OmniAural is a minimal global state manager for React/React-Native applications.

Core Concepts

OmniAural provides global state managment with no need for boiler plate code.

This is done by allowing React components to 'register' for the global state elements they need to be aware of. Those global elements then become part of the component's local state and can be treated like, read-only, local state from the component's point of view.

Changing global state is handled through a call to OmniAural that can be called from any js code (not just components). However, the recommened pattern is to put global state changes in OmniAural actions which are a convenient way to orginize these global state changes. Actions are optional and may not be best suited for all types of projects, but are recommended for most.

Getting started

Instalation

Install package from npm

yarn add omniaural

Initialize

In your top level component (usually App.js) import OmniAural and initialize the global state

import OmniAural from 'omniaural';

OmniAural.initGlobalState({
   account: {
        name: 'Jack',
        phone: '3129058787',
        address: {
            "street": "1st st"
        }
    },
    movies: []
})

Usage

After initialization, global state properties can be (but rarely need to be) accessed directly through the .value() function. See below (Register a component) on the recommened approach to accessing global state values.

OmniAural.state.account.name.value()

Global state properties can be set directly through the .set() function.

OmniAural.state.account.name.set("John")

Global state properties can also be deleted directly through the .delete() function. (This function should be used with care. It can cause UI issues if you are not handling property existence checking in your components)

OmniAural.state.account.name.delete()

Register a component

You can register to listen to a particular property or a whole object on the global state. You can also use aliases to allow for local naming that makes more sense for your component. IMPORTANT - if you create a state object in your component, this must be done before you call OmniAural.register.

import React from 'react'
import OmniAural from 'omniaural'

export class IntroScreen extends React.Component<*, *> {
  constructor() {
    super()
    this.state = {
      person: {
        employed: true
      }
    }

    // Register for the global 'account' (defaults to 'account' in local state)
    // Register for the account.address as 'address' in local state
    OmniAural.register(this, ['account', 'account.address as address'])
  }

  render() {
    return (
      <div style={styles.container}>
        <span style={styles.instructions}>
          Account information
        </span>
        <div style={styles.instructions}>
          {'\n'}
          {`Name: ${this.state.account.name}` /* I'm accessing global state here, it just looks local to the component */}
          {'\n'}
          {`Currently employed: ${this.state.person.employed}` /* this is actually local state */}
          {'\n'}
          {`Street: ${this.state.address.street}` /* this is global state again */}
        </div>
      </div>
    )
  }
}

const styles = {
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white'
  }
}

Update global state

Using the OmniAural.state object, you can make changes to the global state values.

import React from 'react'
import OmniAural from 'omniaural'

export class IntroScreen extends React.Component<*, *> {
  constructor() {
    super()
    this.state = {
      person: {
        employed: true
      }
    }

    OmniAural.register(this, ['account', 'account.address as address'])
  }

  _updateAddress = () => {
      // Updating the street to a hard coded "Main st"
      // Note this is the full global state path
      OmniAural.state.account.address.street.set("Main st")
  }

  render() {
    return (
      <div style={styles.container}>
        <span style={styles.instructions}>
          Account information
        </span>
        <div style={styles.instructions} onClick={this._updateAddress}>
          {'\n'}
          {`Name: ${this.state.account.name}`}
          {'\n'}
          {`Currently employed: ${this.state.person.employed}`}
          {'\n'}
          {`Street: ${this.state.address.street}`}
        </div>
      </div>
    )
  }
}

const styles = {
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white'
  }
}

Adding an action

Actions are the prefered way to encapsulate your global state changes. They can be added from any file in the global space, although it makes sense to group these actions in designated files.

import React from 'react'
import OmniAural from 'omniaural'

// Add a globally accessable action to update the global address object
OmniAural.addAction('updateAddress', (address) => {
    OmniAural.state.account.address.set(address)
})

export class IntroScreen extends React.Component<*, *> {
  constructor() {
    super()
    this.state = {
      person: {
        employed: true
      }
    }

    OmniAural.register(this, ['account.name', 'account.address as address'])
  }

  _updateAddress = () => {
      // call the global action using the name passed into OmniAural.addAction
      OmniAural.updateAddress({street: "Main st"})
  }

  render() {
    return (
      <div style={styles.container}>
        <span style={styles.instructions}>
          Account information
        </span>
        <div style={styles.instructions} onClick={this._updateAddress}>
          {'\n'}
          {`Name: ${this.state.account.name}`}
          {'\n'}
          {`Currently employed: ${this.state.person.employed}`}
          {'\n'}
          {`Street: ${this.state.address.street}`}
        </div>
      </div>
    )
  }
}

const styles = {
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white'
  }
}

Functional components

You can register a functional component by creating an OmniAural state hook useOmniAural

import React from 'react'
import { useOmniAural } from 'omniaural'

const PersonScreen = () => {
  const [person, setPerson] = useOmniAural("account")
  
  return (
    <div>
      <span>User Id: {person.id}</span>
    </div>
  )
}

export default PersonScreen

You can register a functional component to listen to OmniAural state property updates using useOmniAuralEffect

import React from 'react'
import { useOmniAuralEffect } from 'omniaural'

const PersonScreen = () => {
  const [person, setPerson] = useOmniAural("account")

  useOmniAuralEffect(()=> {
    console.log("The account id has changed")
  }, "account.id")
  
  return (
    <div>
      <span>User Id: {person.id}</span>
    </div>
  )
}

export default PersonScreen

You can also register a functional component by wrapping it in the withOmniAural HOC function

import React from 'react'
import { withOmniAural } from 'omniaural'

const PersonScreen = (props) => {
  return (
    <div>
      <span>User Id: {props.person.id}</span>
    </div>
  )
}

export default withOmniAural(PersonScreen, ["account as person"])

API

OmniAural

The main global state manager class. It should be initialized at your top most component (usually App.js). This class will contain your global state. Components can register to specific properties or the whole global state object and update when it changes.

Methods:

initGlobalState()

Initialize the global state with an initial object to which components will register and listen to its changes. You can access and update properties directly from the creates state object.

Parameter Type Description
initialState Object The object with which to initialize your global object.
config Object (Optional) Optional configuration object to set special properties on OmniAural initialization.

Config

Parameter Type Description
pathDelimiter String (Optional) If you plan on using object keys with periods . i.e.(a url path), you will want to change the default delimiter to another character eg. |. Doing so will change string based path interpretation from needing "account.address.street" to needing "account|address|street".
Example:
import { initGlobalState } from 'omniaural';

initGlobalState({
   account: {
        name: 'Jack',
        phone: '3129058787',
        address: {
            "street": "1st st"
        }
    },
    movies: []
})

console.log(OmniAural.state.account.phone.value()) //Prints 3129058787
OmniAural.state.account.phone.set('2125548844')
console.log(OmniAural.state.account.phone.value()) //Prints 2125548844

Can also be used directly on OmniAural:

import OmniAural from 'omniaural';

OmniAural.initGlobalState({
   account: {
        name: 'Jack',
        phone: '3129058787',
        address: {
            "street": "1st st"
        }
    },
    movies: []
})

register()

Register a component to listen to changes on the global state. This method will add the component as a listener to the passed in properties that you want to listen to and call setState on that component whenever a change happens. An alias can be used for each property using the keyword as and will be added to the local state using that alias.

Note: This function must be called after you have initialized your initial local state (if any).

Parameter Type Description
component React.Component The component to be registered as a listener to the global state properties.
paths string Array
listener Function null
Example:
import OmniAural from 'omniaural'

constructor() {
    super()
    this.state = {}

    OmniAural.register(this, ['account as person', 'account.address as address'])
}

addAction()

Actions can be added to OmniAural to be used as batch updates or async calls that need to update the global state after they are completed. You can add an action as a predeclared named function or by passing an anonymous function and a name for it.

Parameter Type Description
action Function The body of this function will be added as a global action. Must be a named function
Example:
import OmniAural from 'omniaural'

const updateAddress = (address) => {
    OmniAural.state.account.address.set(address)
}

OmniAural.addAction(updateAddress)

_onClick = () => {
    OmniAural.updateAddress({street: "Main st"})
}

* If you don't want to use a named function, you can also pass a string as the first argument that will represent the name of the function on OmniAural and an anonymous funtion as a second argument

Parameter Type Description
name String The name of the function that will be save on OmniAural
action Function The body of this function will be added as a global action.
Example:
import OmniAural from 'omniaural'

OmniAural.addAction('updateAddress', (address) => {
    OmniAural.state.account.address.set(address)
})

_onClick = () => {
    OmniAural.updateAddress({street: "Main st"})
}

addActions()

Actions can be added to OmniAural in bulk by passing named functions as arguments to this function

Parameter Type Description
actions Object An object with one or more named functions to be added to OmniAural. Note: All actions must be named functions
Example:
import OmniAural from 'omniaural'

const updateAddress = (address) => {
    OmniAural.state.account.address.set(address)
}

const updateAccount = (account) => {
    OmniAural.state.account.set(account)
}

OmniAural.addActions({updateAccount, updateAddress})

_onClick = () => {
    OmniAural.updateAddress({street: "Main st"})
}

_onAnotherClick = () => {
    OmniAural.updateAccount({address: street: "Main st"}})
}

addProperty()

This function adds new properties to the global state object structure. If you add properties to nested objects, any listener to the parent object will also start listenting to the newly added property.

Parameter Type Description
path String The path in the global state to which to add a property.
value any The value to initialize the newly added property as.
Example:
import OmniAural from 'omniaural'

OmniAural.addProperty("account.id", 4568585)
//or
OmniAural.addProperty("account", {id: 4568585})

getProperty()

This functions accepts a path to a property and returns an object which you can use to get the global value.

Parameter Type Description
path String The path in the global state to the property to get.
Example:
import OmniAural from 'omniaural'

const accountId = OmniAural.getProperty("account.id")
console.log(accountId.value()) // 4568585

setProperty()

This function updates a property at a given path. It receives a string representing the path to the property and a value to update the property with.

Parameter Type Description
path String The path in the global state to the property to update.
value any The value to set the passed in property with.
Example:
import OmniAural from 'omniaural'

OmniAural.setProperty("account.id", 4568585)
//or
OmniAural.setProperty("account", {id: 4568585})

clearProperty()

This function empties an object property at a given path. It receives a string representing the path to the property

Parameter Type Description
path String The path in the global state to the property to update.
Example:
import OmniAural from 'omniaural'

OmniAural.clearProperty("account.address")

HOOKS

useOmniAural

The useOmniAural hook is a custom hook that creates a local variable tied to a global state property value. It can be used in stateless functional components to register to variables that live on global state.

Parameter Type Description
path String A string path that represent the path to the global properties to register to.
Example:
import React from 'react'
import { useOmniAural } from 'omniaural'

const PersonScreen = () => {
  const [accountId, setAccountId] = useOmniAural("account.id")

  return (
    <div>
      <span>User Id: {accountId}</span>
    </div>
  )
}

export default PersonScreen

useOmniAuralEffect

The useOmniAuralEffect hook takes a function and a path (or an array of paths) to properties on the OmniAural state and fires the passed in function when those properties values change.

Parameter Type Description
listener Function A function that will be called when any of the passed in property paths values change
path(s) String or Array A string (or array of strings) that represent the path to the global property to register the listener to.
Example:
import React from 'react'
import { useOmniAuralEffect, useOmniAural } from 'omniaural'

const PersonScreen = () => {
  const [accountId, setAccountId] = useOmniAural("account.id")

  useOmniAuralEffect(() => {
    console.log("Account Id has changed")
  }, ["account.id"])

  return (
    <div>
      <span>User Id: {accountId}</span>
      <div>
      <span>Update Account Id</span>
      <button onClick={() => setAccountId(321321321)}>Update</button>
      </div>
    </div>
  )
}

export default PersonScreen

HOC

withOmniAural()

This function can be used to register a functional component with a collection of properties from the global state using a higher order component. The registered properties will be passed in as props to the functional component.

Parameter Type Description
component Function The functional component to start listening to the global state.
paths Array An array of strings that represent the paths to the global properties to listen to. Each path can receive an alias similar to the register function to be passed into the props.
Example:
import React from 'react'
import { withOmniAural } from 'omniaural'

const PersonScreen = (props) => {
  return (
    <div>
      <span>User Id: {props.person.id}</span>
    </div>
  )
}

export default withOmniAural(PersonScreen, ["account as person"])

Troubleshooting & Gotchas

Most of the errors that OmniAural can throw come from incorrectly listening to nested properties. If you are experiencing a crash referring to accessing property of null, or something similar, you are most likely listening to a property of an object that was either initialized to null or was set to null during some part of execution.

If you know an object can become null, avoid listening to its properties. Rather, listen to the object itself. This way you can code against attempting to access a property of a null object.

Example:
const initialState = {
  account: {
    address: null
  },
  movies: []
}

=========================================

import React from 'react'
import { useOmniAural } from 'omniaural'

const PersonScreen = () => {
  //BAD Because you are listening to a property of a potentially null object and this will crash if address is null
  const [streetAddress, setStreetAddress] = useOmniAural("account.address.street")   

  //GOOD Because you can code against *address* being null, either on initialization or set to null later
  const [address, setAddress] = useOmniAural("account.address")
  const streetAddress = address !== null ? address.street : "Not found"

  return (
    <div>
      <span>Street Address: {streetAddress}</span>
    </div>
  )
}

export default PersonScreen

License

MIT License

Copyright (c) 2020 Isobar North America, Inc. (Chris Steele and Creon Creonopoulos)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.