Make your own state management for React with Proxies and Event emitters
Tags: javascript, react, redux, webdev
Published: 2020-11-25 15:35:38


## Intro
### Easing into the subject
I think it took about 6 months before I got comfortable with `'using Redux'`. 16 months in and I've yet to become comfortable with `'Redux itself'`. Personally, I do realize why Redux is needed in large apps with scalability requirements, and for that matter - redux is a pure god-send. However for the majority of smaller apps, redux's cons could outweigh the pros depending on the circumstances
- Actions are rarely reused
- Being forced to separate logic
- [What Dan says](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367)

![Whatever Dan Abramov says to do](https://pbs.twimg.com/media/Cfh9oRrXIAAiA2Y?format=jpg&name=small)

### What about Context API and other state management libraries?
As with every other package, depending on the project at hand, there could be alternatives that better suit your needs. But why not try making your own? So I started digging into the matter - **what's the easiest way to create a global state management library?**(Yes there is a lot of reasons to not try making your own but bear with me)

### TLDR - the results<a name="tldr"></a>
If you'd rather read the [source code](https://github.com/fl-y/simple-store-hook) ([npm package](https://www.npmjs.com/package/pstore))

### Ok, but why would I make one myself?
- What better way to show interest in a subject than say 'I tried making one myself, here are the results'. Possibly the best interview question answer.(Obviously after a lengthly description about various state management libraries and your experiences)
- Demystifying the possibly vague concept and mechanisms of global state management.
- With an understanding of how to start off, customizing for your project might take less time in setting up than actually easing into other global state management like redux which have quite the learning curve.
- Honestly there's not much reason, I just though I'd share my experience in the form of a tutorial. Learning redux(if you already haven't) is far more beneficial for most people and large scale app scenarios.

### Why proxies and events instead of useState and hooks
So before I begun tackling the matter, I wanted to avoid making anything from React mandatory for the following reasons
- To make React optional(obviously)
- Finer controls over the store
- Most importantly, make the store updatable without having to drill update functions from a React component.

Personally I was fed up with having to drill store dispatchers through multiple functions since I had begun to move onto a more javascript focused coding style. My first attempt was by using rxjs's observers and observables to make this possible. It worked, but the rxjs dependency felt heavy for sites that needed minimal bundle size. So after a fair bit of researching, proxies paired with events felt like the perfect choice.

### [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
`The closest thing that mimics c++ operator overloading in js` would be my first impression.
But in reality it's a wrapper that allows you to define custom functionality for otherwise un-editable functions. Pair it with Reflect, and you can keep normal functionality and just have side effects.(This is a personal opinion and can be disputable - if so, let me know in the comments)
```javascript
const store = {};
const storeProxy = new Proxy(store, {  
	set: function (obj, prop, value) {  
		obj[prop] = value;
		//	my custom set logic
		//....
		console.log(`I'm setting ${prop} to - `, value);
		return true;  
	},
	get: function (target, prop, receiver) {
		const obj = Reflect.get(...arguments);
		//	my custom get logic
		//...
		return obj;
	}
});
```
Now if you edit the store using the storeProxy like this
```javascript
storeProxy.foo = "bar";
```
You'll see the custom set logic being executed. Kind of like an observer observing an observable!
On a sidenote, try creating an array with about 10 values, create a proxy that counts set operations, then pop a value and shift a value. You'll see why shifting values take O(n) time while popping take O(1) quite visually.

### [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
Using [CustomEvents](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) and dispatching to the DOM works as well when using pure React. However in scenarios where the DOM is inaccessible (SSR or SSG using [Nextjs](https://nextjs.org/) for example), that could not be an option. Also events from event emitters have  less dead-weight since they do not propagate or bubble anywhere.

## Walkthrough

I eventually refactored my codebase to a Class based approach, but we'll do a functional approach for the sake of a wider audience.

**Disclaimer** *I did not try out any of this code and there could be mistakes. Any form of constructive criticism is appreciated. The code below should serve as a guideline but could also work as intended. No promises :). The github repo in the [TLDR section](#tldr) is working code.*

### Step 1 - The building blocks
```javascript
//  because using document events doesn't work on SSG / SSR  
const Emitter = require("events")
const EventEmitter = new Emitter()
  
//  virtually no limit for listeners  
EventEmitter.setMaxListeners(Number.MAX_SAFE_INTEGER)  
  
let eventKey = 0  
export const createStore = (initObj) => {  
	//  underbar for private methods / vars  
	const _evName = `default-${eventKey++}`
	
	const _store = cloneDeep(initObj) //  preferred deep cloning package, recommend rfdc
	
	const _storeProxy = new Proxy(store, {
		set: function (obj, prop, value) {
			//  apply options, restrictions pertaining to your needs
		}
	});
	
	//  dispatch logic to use when store is updated  
	const _dispatchEvent = () => {  
		EventEmitter.emit(_evName)  
	}
	// ... the HOC and update logic
}
```
So this is the barebones version. Bear with me.
Underbars are in front of all declarations to simulate private declarations that won't be exposed outside.
_evName is defined so that events can be distinguished among multiple stores

### Step 2 - The HOC and update logic
```javascript
// ... the HOC and update logic
	const updateStore = obj => {  
		 //  only update store when obj has properties
		if(Object.getOwnPropertyNames(obj).length < 1) return;
		//  update logic via storeProxy
		Object.getOwnPropertyNames(obj).forEach(key => {
			//	possible custom logic
		    _storeProxy[key] = obj[key];
	    });
	    //  dispatch for EventEmitter
	    _dispatchEvent();
    }
	
	const getStore = () => return {..._store};
    
	const createUseStore = () => {  
		//  purely for rerendering purposes  
		const [dummy, setDummy] = useState(false);  
		const rerender = useCallback(() => setDummy(v => !v), [setDummy]);  
		
		useEffect(() => {  
			const eventHandler = () => rerender();  
			EventEmitter.on(_evName, eventHandler);  
			return () => EventEmitter.removeListener(_evName, eventHandler);  
		}, [rerender]);  
	    
	    //  only updates when the above event emitter is called
	    return useMemo(() => {
		    return [this._store, this.updateStore];
	    }, [dummy]);
    }
    return [createUseStore, updateStore, getStore];
}
```
The actual update logic and the HOC are suddenly introduced and step 1 starts to make sense. The code is possibly simple enough to understand as it is, but here's how the logic goes.
- An event emitter is defined(globally)
- A store in the form of a js object is created
- A proxy is created that proxies the store with custom logic.
- updateStore is defined that sets the value for each key to the proxy, then dispatches the event
- getStore is defined that returns the current store deep-cloned.
- A HOC is defined that returns the store and update function.

### Step 2.5 - Step 2 [MVP](https://en.wikipedia.org/wiki/Minimum_viable_product) in action

```javascript
import {createStore} from "where/you/put/your/createStore";

const initMyStore = {
  foo: bar
};
const [createUseMyStore, updateMyStore, getMyStore] = createStore(initMyStore);
const useMyStore = createUseMyStore();

export { useMyStore, updateMyStore, getMyStore };
```

```javascript
import * as React from "react";
import {useMyStore} from "the/initcode/above";

export default function MyComponent() {
	const [store] = useMyStore();
	return (
		<div>{store?.foo}</div>
	)
}
```
```javascript
//	in another file far far away.....
import {updateStore} from "the/initcode/above";

function aFunctionNestedInside50Functions () {
	updateStore({foo: "barbar"});
}
```
As stated above this is a barebones MVP, meaning that a LOT of core functionality that are usually expected for a global state management package are currently stripped away such as
- selective event dispatching
- selective property watching
- immutability or selective immutability
- [Container predictability](https://stackoverflow.com/questions/54846175/redux-is-a-predictable-state-container)
- A LOT of safeguards that other global state management packages supply by default.

For the majority of simple apps, the above code + returning a deep copied / deep frozen version on 'get' should be enough.
Let's try expanding functionality to allow selective state updates and event dispatches

### Step 3 - Functionality expanding

```javascript
	//...

	//  dispatch logic to use when store is updated
	//	updated keys are emitted to event emitter
	const _dispatchEvent = (keys) => {
		EventEmitter.emit(_evName, keys)
	}
	// ... the HOC and update logic
	const updateStore = obj => {
		//  only update store when obj has properties
		if(Object.getOwnPropertyNames(obj).length < 1) return;
		//	keys are stored to pass to dispatchEvent
		let keys = [];
		//  update logic via storeProxy
		Object.getOwnPropertyNames(obj).forEach(key => {
			//	possible custom logic
			_storeProxy[key] = obj[key];
			keys.push(key);
		});
		
		if(keys.length < 1) return;
		//  dispatch for EventEmitter
		_dispatchEvent(keys);
	}
	
	const getStore = () => return {..._store};
	
	//	watch - which key of the store to watch
	const createUseStore = (watch) => {  
		//  purely for rerendering purposes  
		const [dummy, setDummy] = useState(false);  
		const rerender = useCallback(() => setDummy(v => !v), [setDummy]);  
		
		useEffect(() => {  
			const eventHandler = keys => {
				//	Don't rerender if property to watch are not part of the update keys
				if(watch && !keys.includes(watch)) return;
				rerender();
			}
			EventEmitter.on(_evName, eventHandler);  
			return () => EventEmitter.removeListener(_evName, eventHandler);  
		}, [rerender, watch]);  
	    
	    //  only updates when the above event emitter is called
	    return useMemo(() => {
		    //	return watched property when watch is defined.
		    if(watch) return [this._store[watch], this,updateStore];
		    return [this._store, this.updateStore];
	    }, [dummy, watch]);
    }
    return [createUseStore, updateStore, getStore];
}
```

A lot is going on here, but all for the functionality to be able to only have state updates when the 'watched' property is updated. For instance if the store was initialized like
```json
{
	foo: "bar",
	fee: "fi",
	fo: "fum",
}
```
and a component was like
```javascript
export default function myComp () {
	const [foo, updateStore] = useMyStore("foo");
	return <>{foo}</>
}
```
This component will not be updated by
```javascript
updateStore({fee: "newFi", fo: "newFum"});
```
but only when 'foo' is updated, which is one of the main functionalities that I wished to implement when I set out on this bizarre journey.
A lot more functionality with a class based approach is done in the github repo mentioned above so check it out if you're interested.

### Conclusion
I don't know about you, but when I started to create my own version of a personalized state management library, creating new functionality for my global state was simply ***enjoyable*** - something I rarely experienced while fiddling around with redux, possibly [yak shaving](https://americanexpress.io/yak-shaving/) my time away. But jokes aside, for most use cases doing this is the pure definition of 'reinventing the wheel', so please implement and try out at your own discretion - a fun side project without heavy reliance on global state is a scenario I would personally recommend.