Skip to content

Instantly share code, notes, and snippets.

@jakoblorz
Created May 17, 2019 16:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jakoblorz/6126582685f93bbe2313e2775a4b233f to your computer and use it in GitHub Desktop.
Save jakoblorz/6126582685f93bbe2313e2775a4b233f to your computer and use it in GitHub Desktop.
Simple global state without depending on any additonal state manager. Uses and exposes React Hooks.

I used this structure in multiple projects and had a blast using it which is why I want to share it. It automatically propagates state changes to all dependent states and does not require additional state managing packages such as redux or mobx.

It enables architectures where e.g. the navbar is at the same level as all other components while still receiving updates about e.g. current user: If in this example the Index View logs the user in, there will also be a re-render in the navbar as its state has changed. This construction removes the state-silos of each component. Using useState instead of useReflectedState still enables the usage of component-specific state.

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

import Navbar from "./views/Navbar";
import Index from "./views/Index";
import State from "./State";

ReactDOM.render((<Router>
  <State />
  <Navbar /> // <- Navbar is not a child of Index, thus would normally not receive state updates
  <main>
    <Route path="/" exact component={Index} />
  </main>
</Router>), document.getElementById("root"));
import React, { Component, Dispatch, SetStateAction, useEffect, useState } from "react";
// Define a global State object
export type State = {
/* "username": string */
}
export class Store<T> {
private data: T = {} as T;
private subscriptions: { [x: string]: { [x: string]: (x: any) => Promise<void> | void }} = {};
constructor(init: T) {
this.data = init;
}
public async fromString(s: string) {
const update = JSON.parse(s);
for (const key in Object.keys(update)) {
await this.set(key as keyof T, update[key]);
}
}
public toString(): string {
return JSON.stringify(this.data);
}
public async set<K extends keyof T>(key: K, val: T[K]) {
if (this.data[key] === val) {
return;
}
this.data[key] = val;
if (!this.subscriptions[key as string]) {
this.subscriptions[key as string] = {};
}
await Promise.all(Object
.keys(this.subscriptions[key as string])
.map((k) => this.subscriptions[key as string][k])
.map((fn) => fn(val)));
}
public get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
public sub<K extends keyof T>(key: K, fn: (x: T[K]) => Promise<void> | void): () => void {
const id = `${Math.random() * 1000}`;
if (!this.subscriptions[key as string]) {
this.subscriptions[key as string] = {};
}
this.subscriptions[key as string][id] = fn;
return (() =>
delete this.subscriptions[key as string][id]
).bind(this);
}
}
export default class StateService extends Component {
public static getStore(): Store<Partial<State>> {
return StateService.Store;
}
public static async saveCurrentState() {
/* await AsyncStorage.setItem("state", StateService.Store.toString()); Save current state to disk */
}
public static async loadCurrentState() {
/* const state = await AsyncStorage.getItem("state"); Load state from disk */
if (state != null) {
StateService.Store.fromString(state);
}
}
// monostate store
private static Store = new Store<Partial<State>>({});
public async componentDidMount() {
await StateService.loadCurrentState();
}
public async componentWillUnmount() {
await StateService.saveCurrentState();
}
public render() {
/* return (<div></div>); Expose something that can be rendered */
}
}
// behaves just like setState, only that global state changes are locally propagated
export function useReflectedState<K extends keyof State>(key: K): [Partial<State>[K], Dispatch<SetStateAction<State[K]>>] {
// base on an actual useState Hook, initialize with existing state
const [val, setVal] = useState(StateService.getStore().get(key));
// sub returns a function to cancel the subscription, which works very well in this context (useEffect Hook)
useEffect(() => StateService.getStore().sub(key, (update: Partial<State>[K]) => {
setVal(update);
}), []);
return [
val,
(value: SetStateAction<State[K]>) => {
StateService.getStore().set(key, value as State[K]);
},
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment