How to write a super simple Reactive State Manager

calendar icon
29 Feb
2024
31 Jan
2022
scroll

Every application needs a state management system to have the ability to react to changes in the data. There are lots of state managers for every taste, from easy to understand ones to mind-breaking.

Do you know how they work? What principles stand behind them? I'm sure you are. But these questions I asked myself not a long time ago, and in my opinion, it is still unknown territory for beginners. So, shall we go in?

Behind most managers stands the <mark>Observer</mark> pattern. It is a powerful pattern. It says that there is a <mark>subject</mark> - a particular object encloses some data, and there are <mark>observers</mark> - objects that want to know when that data changes and what value it has now.

How will they know about the change? The <mark>subject</mark> should tell them that he is changed. For that, every <mark>observer</mark> should ask the <mark>subject</mark> to notify it when something happens. It is a <mark>subscription</mark>.

And when some data changes, the subject notifies all known observers about that. That is a <mark>notification</mark>.

Pretty simple, yeah?

Practically, there are many implementations for this pattern. We are going to show the simplest one.

Basically, the data of your application aggregates into a restricted scope. In JavaScript, we can use an object for that purpose. Each key represents a separated independent chunk of the data.


const state = {
	key1: 'some useful data',
	key2: 'other useful data',
	// and so on
};

We can freely read and change these chunks as we want. But the problem is that we cannot predict when the change happens and what piece is changed with what value. Simply put, the object isn't reactive. Fortunately, JavaScript has a feature that helps us track any action that is made with any object. Its name is <mark>Proxy</mark>.

Proxy is a wrapper around the object which can intercept and redefine fundamental operations for that object (MDN resource).

By default, <mark>Proxy</mark> passes through all operations to the target object. To intercept them, you need to define traps. A trap is a function whose responsibility is to redefine some operation.

All operations and their trap names you can find here.

With this ability, we can write our initial <mark>store</mark> function. In the end, we should be able to do this:


const appState = store({ data: 'value' });

// Subscribe to the data changes.
appState.on('data', (newValue) => {
	// do something with a newValue
});

// Somewhere in the code
appState.data = 'updated value'; // observer is invoked

As I said earlier, the <mark>subject</mark> (our object with some data) should notify <mark>observers</mark> (some entities) when its data was changed. That can be made only when the <mark>subject</mark> knows what entities want to receive notifications. That means that the <mark>subject</mark> should have a list of <mark>observers</mark> inside.


const store = (target) => {
	const observers = [];

	return new Proxy(target, {});
};

And now, we should define a trap for assigning a new value to the target object. That behaviour defines a <mark>set</mark> interceptor.


const store = (target) => {
	const observers = [];

	return new Proxy(target, {
		set: (target, property, value) => {
			target[property] = value;
			observers
				.filter(({ key }) => key === property)
				.forEach(({ observer }) => observer(value));
			return true;
		},
	});
};

After updating the value, the <mark>subject</mark> notifies all <mark>observers</mark> that were added to the list of observers. Great! We've created a notification behaviour. But how does the <mark>subject</mark> add an <mark>observer</mark> to the subscription list?

The answer is that the <mark>subject</mark> should expose a way to trigger this subscription. With <mark>Proxy</mark> in mind, we can define a virtual method that will accomplish that process. How can we do that?

Virtual method is a method that doesn't exist in the target object, but <mark>Proxy</mark> emulates it by creating it outside of the target object.

As we know, a method is a property which value is a function. That tells us that we should define a <mark>get</mark> interceptor and provide a handler for an absent property. At the same time, we shouldn't block access to the target's properties.


const store = (target) => {
	const observers = [];

	return new Proxy(target, {
		get: (target, property) =>
			property === 'subscribe'
				? (key, observer) => {
						const index = observers.push({ key, observer });
						return () => (observers[index] = undefined);
				  }
				: target[property],
		set: (target, property, value) => {
			target[property] = value;
			observers
				.filter(({ key }) => key === property)
				.forEach(({ observer }) => observer(value));
			return true;
		},
	});
};

You may notice that the execution of the <mark>subscribe</mark> function returns another function. Yes, indeed. Observers should be able to stop listening to changes when they want to. That's why <mark>subscribe</mark> returns a function that will delete the listener.

And that's it! We may want to make deleting a property reactive. As we did earlier, a <mark>delete</mark> interceptor is for that.


const store = (target) => {
	const observers = [];

	return new Proxy(target, {
		get: (target, property) =>
			property === 'subscribe'
				? (key, observer) => {
						const index = observers.push({ key, observer });
						return () => (observers[index] = undefined);
				  }
				: target[property],
		set: (target, property, value) => {
			target[property] = value;
			observers
				.filter(({ key }) => key === property)
				.forEach(({ observer }) => observer(value));
			return true;
		},
		deleteProperty: (target, property) => {
			delete target[property];
			observers
				.filter(({ key }) => key === property)
				.forEach(({ observer }) => observer(undefined));
			return true;
		},
	});
};

And now our <mark>store</mark> function is complete. There are a lot of places for improvements and enhancements. And it is up to you! 🤗

Also, you can see a slightly better implementation in our @halo/store package. A code from these examples lives in the <mark>store.js</mark> file. But there is one more entity that is worth explaining. That's why we plan to write the next article precisely about it where we are going to explain the purpose of the package and in what situations you may need it. Hold tight and cheer up!

Writing team:
No items found.
Have a project
in your mind?
Let’s communicate.
Get expert estimation
Get expert estimation
expert postexpert photo

Frequently Asked Questions

No items found.
copy iconcopy icon

Ready to discuss
your project with us?

Please, enter a valid email
By sending this form I confirm that I have read and accept the Privacy Policy

Thank you!
We will contact you ASAP!

error imageclose icon
Hmm...something went wrong. Please try again 🙏
SEND AGAIN
SEND AGAIN
error icon
clutch icon

Our clients say

The site developed by Halo Lab projected a very premium experience, successfully delivering the client’s messaging to customers. Despite external challenges, the team’s performance was exceptional.
Aaron Nwabuoku avatar
Aaron Nwabuoku
Founder, ChatKitty
Thanks to Halo Lab's work, the client scored 95 points on the PageSpeed insights test and increased their CR by 7.5%. They frequently communicated via Slack and Google Meet, ensuring an effective workflow.
Viktor Rovkach avatar
Viktor Rovkach
Brand Manager at felyx
The client is thrilled with the new site and excited to deploy it soon. Halo Lab manages tasks well and communicates regularly to ensure both sides are always on the same page and all of the client’s needs are addressed promptly.
Rahil Sachak Patwa avatar
Rahil Sachak Patwa
Founder, TutorChase

Join Halo Lab’s newsletter!

Get weekly updates on the newest stories, posts and case studies right in your mailbox.

Thank you for subscribing!
Please, enter a valid email
Thank you for subscribing!
Hmm...something went wrong.