Dim: Async State Management
I'm working on creating something I can call "functional web components".
Following the previous article explaining how we can create functional web components, we have the basics to put together an app. State management in such an approach is typically top-down to make the rendering predictable. However, I wanted to explore if there are any benefits to define and manage state in web components with a bottom-up approach. I wanted to see if it could give me a greater flexibility in developing an UI and not having to worry about the state management.
I wanted to create a hook that would work something like this:
const {
form: {
input: [inputValue, setInputValue],
},
todos: [todos, setTodos],
} = useStore({
form: {
input: useState(""),
},
todos: useState([]),
});
The function input could be seen as kind of a schema for the state and the return value would the the same object shape with the value ansd setter like you would have in React hooks. if any component would use this store with this input shape, it would be able to subscribe-to and set changes.
I guess this is against the principles of functional programming, but i decided to try with event listeners and custom events. This way if a setState
is called, it can trigger a custom event. All other components that are listening to this event can update their state.
Creating the useStore hook
The useStore
hook is a function above simply returns an object with the state and the setter from the useState
hooks in the form
and todos
properties.
export const useStore = (store) => {
return store;
};
This function is quite simple. The useStore
function is a simple function that returns the store object. The store object is passed as an argument to the useStore
function. The store object is a simple object with the form
and todos
properties. The form
property is an object with the input
property that is an array with the value and setter from the useState
hook. The todos
property is an array with the value and setter from the useState
hook.
Creating the AsyncronousStateManager
We inspect the shape of the store object to create identifiers for the properties we want to listen to. We can then create a custom event listener for each property. we would need some kind of storage manager to handle the state and the listeners.
class AsyncronousStateManager {
private store: any = {};
private eventListener: any[] = [];
generateListener(listener) {
const { listenerId, key, value } = listener;
// Remove existing event listeners if they exist
const existingListender = this.eventListener.find(
(listener) => listener.listenerId === listenerId
);
const listenerName = `${key}`;
if (existingListender) {
window.removeEventListener(listenerName, existingListender.listener);
this.eventListener = this.eventListener.filter(
(listener) => listener.listenerId !== listenerId
);
}
// Create a new event listener
const newListener = (event) => {
value[1](event.detail);
this.store = {
...this.store,
[key]: event.detail,
};
};
window.addEventListener(listenerName, newListener);
this.eventListener.push({
listenerId,
listener: newListener,
});
const newSetter = (newValue) => {
window.dispatchEvent(
new CustomEvent(listenerName, {
detail: newValue,
})
);
};
const newState = this.store[key] || value[0];
return [newState, newSetter];
}
removeListeners(listenerId) {
const existingListender = this.eventListener.find(
(listener) => listener.listenerId === listenerId
);
if (existingListender) {
window.removeEventListener(
`${existingListender.key}`,
existingListender.listener
);
this.eventListener = this.eventListener.filter(
(listener) => listener.listenerId !== listenerId
);
}
}
}
const asyncronousStateManager = new AsyncronousStateManager();
Lets break this down.
- The
AsyncronousStateManager
class is a class that has astore
property that is an object that holds the state. TheeventListener
property is an array that holds the event listeners. - The
generateListener
method is a method that generates a new event listener. The method takes a listener object as an argument. The listener object has alistenerId
,key
, andvalue
properties. ThelistenerId
property is a unique identifier for the listener. Thekey
property is the key of the state in the store object. Thevalue
property is an array with the value and setter from theuseState
hook. - The
generateListener
method first removes existing event listeners if they exist, then creates a new event listener. this is so it always uses the latest state. The new event listener updates the state in the store object and calls the setter from theuseState
hook. - The
removeListeners
method is a method that removes event listeners. The method takes a listenerId as an argument then removes the event listener with the given listenerId from theeventListener
array.
Creating the createListeners function
Now that we have a way to create and remove event listeners, we can now use it to event listeners that trigger state updates. lets create a function to traverse the store object and create event listeners for each property.
const createListeners = (store, listenerId) => {
const traverse = (obj, path) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === "object" && obj[key].length === undefined) {
traverse(obj[key], `${path}${key}.`);
} else {
const [asyncState, asyncSetState] =
asyncronousStateManager.generateListener({
listenerId,
key: `${path}${key}`,
value: obj[key],
});
obj[key] = [asyncState, asyncSetState].concat(obj[key]);
}
});
};
traverse(store, "");
};
The createListeners
function is a function that creates event listeners for each property in the store object. The function takes the store object and a listenerId as arguments. The function uses a traverse
function to traverse the store object and create event listeners for each property.
Putting it all together
Now lets update our useStore hook to put it all together
export const useStore = (store) => {
const [randomId] = useState(crypto.getRandomValues(new Uint8Array(8)));
createListeners(store, randomId);
useEffect(() => {
return () => {
asyncronousStateManager.removeListeners(randomId);
};
}, []);
return store;
};
Demo
Conclusion
I dont recomend using this approach to create any production ready applications. This is more of an experiment to see if we can create asynchrounous state management for web components. Im sure there are more changes that can be made to make it better. I would like to hear your thoughts on this approach and if you think it could be improved in any way.