globstate

1.1.8 • Public • Published

GlobState

Super simple global state management for React JS and preact.

Managing global state in React/preact can be hard and the Redux library is overkill for many applications and can result in overly verbose and complicated code when very simple and easy to understand mechanisms often do just as fine.

Installation

npm i --save globstate

or

yarn add globstate

How to use

We will use the example of a site wide search field here. You have two steps:

.1. Construct a class for the state with methods to modify it, and export an object of that class from the file:

// globals/search.js
import GlobState from 'globstate'
 
class Search extends GlobState {
    _results = null
 
    constructor() {
        super('search')
    }
    results() {
        return this._results
    }
    doSearch(text) {
        fetch('https://example.org/api/search?q='+text).then(r => r.json()).then(json => {
            this._results = JSON.parse(json)
            this.updateComponents() // This triggers a re-render on all connected components
        })
    }
}
 
// Note that we export an object of type Search, not the class itself
export default new Search()

.2. Create a component that uses that global object and gets updated each time it changes:

// SearchResults.jsx
import GlobState from 'globstate'
import search from './globals/search'
 
class SearchResults extends Component {
    render() {
        // Note that in the render function you can access the global `search` object directly
        return (
            <ul>
                {search.results().map(result => (
                <li>{result.title}</li>
                ))}
            </ul>
        )
    }
}
 
// Export a connected version of the component that will update every time the search results change
export default GlobState.connect(SearchResults, search)
// SearchField.jsx
import search from './globals/search'
 
class SearchField extends Component {
    constructor() {
        super()
        this.state = {
            text: ''
        }
    }
    render() {
        return (
            <form onSubmit={e => {e.preventDefault(); this.submit(); return false}}>
                <input type="text" value={text} onInput={e => this.setState({text: e.target.value})}/>
                <button type="submit">go</button>
            </form>
        )
    }
    submit() {
        search.doSearch(this.state.text)
    }
}
// App.jsx
import GlobState from 'globstate'
import search from './globals/search'
import SearchArea from './SearchArea'
import SearchField from './SearchField'
 
class App extends Component {
    render() {
        <div>
            {search.results!==null && (
                <p>Info: There are {search.results().length} results  <p/>
            )}
            <SearchField/>
            <SearchArea/>
        </div>
    }
}
App = GlobState.connect(App, search)

That's it! You can use several global objects in the same component without problems and also use asynchronous methods (like setTimeout or API calls) to update global objects.

How it works

Global state is actually really simple. To make it work you need to have some global object that

  1. can be read and modified from everywhere in your web app
  2. updates the components that use it when it changes

The first part is easy, just make an object that has variables and functions, put it in a file and wherever you need it, just import/require the file. In Javascript every file itself is loaded exactly once and thus is a global object already:

// myGlobalCounter.js
class MyGlobalCounter {
    _value = 0
 
    read() {
        return this._value
    }
    increment() {
        this._value += 1
    }
}
const myGlobalCounter = new MyGlobalCounter()
export default myGlobalCounter

So how to update all necessary components when the counter gets incremented? Components re-draw themselves whenever their state changes, their props change or if forceUpdate() is called. We will use the latter. And some subscription system is needed as well:

// myGlobalCounter.js
class MyGlobalCounter {
    _value = 0
    _subscribers = []
 
    subscribe(component) {
        this._subscribers.push(component)
    }
    unsubscribe(component) {
        const index = this._subscribers.indexOf(component)
        if (index !== -1)  this._subscribers.splice(index, 1)
    }
    updateComponents() {
        for (const subscriber of this._subscribers) {
            subscriber.forceUpdate()
        }
    }
    read() {
        return this._value
    }
    increment() {
        this._value += 1
        this.updateComponents()
    }
}
const myGlobalCounter = new MyGlobalCounter()
export default myGlobalCounter
// CounterDisplay.jsx
import counter from './MyGlobalCounter'
 
class CounterDisplay extends Component {
    componentWillMount() {
        counter.subscribe(this)
    }
    componentDidUnmount() {
        counter.unsubscribe(this)
    }
    render() {
        return (
            <div>
                Counter: {counter.read()}
                <span onClick={e => counter.increment()}>+</span>
            </div>
        )
    }
}

Now you can use the component in multiple places in your web app and they are all connected to each other through the myGlobalCounter variable. Whenever it changes it calls its updateComponents method which will call forceUpdate on all subscribed compnents. This is by the way exactly how React Redux does (or, at least, did) it as well!

The subscription management should go into its separate class of course. And to shorten all the subscribing/unsubscribing you can create a simple High Order Component (HOC) that takes care of that:

// Updater.js
class Updater {
    _subscribers = []
 
    subscribe(component) {
        this._subscribers.push(component)
    }
    unsubscribe(component) {
        const index = this._subscribers.indexOf(component)
        if (index !== -1)  this._subscribers.splice(index, 1)
    }
    updateComponents() {
        for (const subscriber of this._subscribers) {
            subscriber.forceUpdate()
        }
    }
    static connect(BaseComponent, ...globals) {
        return class extends Component {
            componentWillMount() {
                for (const g of globals) {
                    g.subscribe(component)
                }
            }
            componentWillUnmount() {
                for (const g of globals) {
                    global.unsubscribe(component)
                }
            }
            render() {
                return <BaseComponent {...this.props}/>
            }
        }
    }
}

And Updater here is basically GlobState. See, global state management can be that simple!

Examples

Counter

The usual example, a counter:

// globals/counter.js
const GlobState = require('globstate')
 
class Counter extends GlobState {
    _value = 0
 
    read() {
        return this._value
    }
    increment() {
        this._value += 1
        this.updateComponents()  // This will update all subscribed components
    }
}
 
export default new Counter()
// Counter.jsx
import counter from './globals/counter'
 
class Counter extends Component {
    render() {
        return (
            <div>
                Counter: {counter.read()}
                <span onClick={e => counter.increment()}>+</span>
            </div>
        )
    }
}
 
export default GlobState.connect(Counter, counter)

User management

Many web apps have a mechanism to login, register and logout a user. The user then of course is a global state and can be managed with the GlobState library. This example includes async state changes as login/register/logout are done via API calls.

// globals/user.js
const GlobState = require('globstate')
 
class User extends GlobState {
    loggedin = false
    username = false
 
    constructor() {
        super()
    }
 
    login(email, password, next) {
        setTimeout(() => {  // Simulate an api call..
            if (email==='some@one.com' && password==='123') {
                this.loggedin = true
                this.username = 'Michel'
                next(null) // tell caller that we finished
            } else {
                this.loggedin = false
                next(new Error('Wrong credentials!'))
            }
            this.updateComponents()
        }, 500)
    }
}
 
export default new User()
// App.jsx
import user from './globals/user'
 
// Just some component that uses the global user object
class SomeComponent extends Component {
    render() {
        return (
            <div>
                {user.loggedin? (
                    <span>Logged in!</span>
                ) : (
                    <span>Not logged in!</span>
                )}
            </div>
        )
    }
}
SomeComponent = GlobState.connect(SomeComponent, user)
 
class Header extends Component {
    render() {
        return (
            <header>
                {user.loggedin? (
                <span>Hello {user.username}!</span>
                ) : (
                <button type="button" onClick={e => this.tryLogin()}>login</button>
                )}
            </header>
        )
    }
    tryLogin() {
        const email = prompt('E-Mail')
        const password = prompt('Password')
        user.login(username, password, (err) => {
            if (err) alert(err.message)
        })
    }
}
Header = GlobState.connect(Header, user)
 
function App() {
    return (
        <div>
            <Header/>
            <main>
                <SomeComponent/>
            </main>
        </div>
    )
}

License

MIT

Package Sidebar

Install

npm i globstate

Weekly Downloads

0

Version

1.1.8

License

MIT

Unpacked Size

64.1 kB

Total Files

20

Last publish

Collaborators

  • badestrand