So I am currently working on a note taker web app, that has some features that I thought would better work with a state management tool like zustand, I never used it before, so I want to use it, but I thought, why not learn its internal working first, so I thought of making a mini zustand, mostly consisting of core features.
Some concepts to know before:
Closures: This is one of the core features of javascript, and it is most helpful in understanding the concept behind the working of this library. I will just give you bare minimum to understand,
A closure in JavaScript is a function that "remembers" the environment in which it was created, allowing it to access variables from its outer (enclosing) function's scope, even after the outer function has finished executing.
function createCounter() { let count = 0; // This variable is part of the outer function's scope return function() { // This is the inner function (the closure) count++; // It "remembers" and can access 'count' return count; }; } const counter1 = createCounter(); console.log(counter1()); // Output: 1 console.log(counter1()); // Output: 2
Pub-Sub relationship: In simple terms, you can take an example of a magazine subscription, whenever a new issue is published, every subscriber is notified of the new issue, we'll use this feature to render state changes to all the components that will be affected by this.
React state changes: In react if you use a variable to just make some changes in the state, react will not "remember" this and between renders the value will get lost, and we certainly don't want that, to update the state, you have to use the useState hook to update the state, we'll use this to send the information to React to make changes in state.
Now concepts out of way,
Let's start building, shall we
First Step: To make a simple store first
We'll take a simple counter to make, and store will be handling all the states on a central location, so we don't have to worry about states everywhere.
const createStore = initialState => { let state = initialState const getState = () => state const setState = (newState) => { state = {...state, ...newState} } return {getState, setState} } const counterStore = createStore({count: 0}) console.log(counterStore.getState()) counterStore.setState({count: 1}) console.log(counterStore.getState())
Here we make a simple createStore function that will have the state variable inside, not directly accessible, and here the role of closures comes in, we access the state variable by two functions:
first is the getter, getState
function that will return the current value of the state and
second is the setter, where we provide newState
and it will overwrite the current state. And after that we create a instance of this createStore
function as counterStore
so that we can use it
It works as a simple store now, but we have two things to take care of,
first is how we can send change updates to all that is associated with the store and
second to link it in react,
To fix the first issue, we have to,
Second Step: To introduce subscribers
Subscribers will be a set of listeners
(callback function) that will be updated based on changes made in the state
const createStore = initialState => { let state = initialState const subscribers = new Set() //used set since we do not want duplicates const getState = () => state const setState = (newState) => { state = {...state, ...newState} subscribers.forEach(cb => cb(state)) // we update each subscriber on the changes } const subscribe = listener => { subscribers.add(listener) return () => subscribers.delete(listener) //returning a function that will unsubscribe } return {getState, setState, subscribe} } const counterStore = createStore({count: 0}) const unsubscribe = counterStore.subscribe(state => console.log("state changed: ", state)) counterStore.setState({count: 1}) counterStore.setState({count: 2}) counterStore.setState({count: 3}) unsubscribe() counterStore.setState({count: 4})
Here, subscribers are an empty set
to avoid duplicates, these subscribers will be updated based on changes on state, in setState function we use forEach
to update the state changes and, I implemented a subscribe
function, in which a listener is added to the subscriber set, and we return a function that will remove the listener from the set.
Consequently, you can notice that after unsubscribing the there is no log, that's the desired effect.
So, the core of our mini zustand is complete, but a last issue still remains,
how can we use this in our react app?
To solve this, we'll implement a custom hook, that will update the state, in react based on state changes in store.
Third Step: useStore custom hook
import {useEffect, useState} from 'react' export const useStore = store => { const [state, setState] = useState(store.getState()) useEffect(() => { const unsubscribe = store.subscribe(setState) return unsubscribe }, [state]) return state }
This is the useStore
custom hook, that will take care of state changes and tell react to update the state based on changes in state in our store, in this we take store as a param and use it to first pass the initial state to the "state" state in useState hook, after that a useEffect is used to subcribe
to the changes, as well as return the unsubscribe function during cleanup, and finally our state and this is pretty much our mini zustand is done,
Now to test it that, if this works or not
As you can see it is working as expected, to test it i made two components, one contains the counter display (state) and other contains the increment button,
import counterStore from "./createStore" import {useStore} from './useStore' const ComponentOne = () => { const state = useStore(counterStore) return ( <div> <h1>ComponentOne</h1> <h2>{state.count}</h2> </div> ) } export default ComponentOne
import counterStore from "./createStore" const ComponentTwo = () => { const increment = () => counterStore.setState({ count: counterStore.getState().count + 1 }) return ( <div> <h1>ComponentTwo</h1> <button onClick={increment}>increment</button> </div> ) } export default ComponentTwo
And at last make sure to make a counterStore instance to use it,
const counterStore = createStore({count: 0}) export default counterStore
So that's it, that is your state management library core is done, there are further improvements, that can be done in this, like
selecting a single state, because currently it will target the whole state, or
making persistent store like in zustand middleware, but that's a story of another post.
Please tell me in the comments, will you try to make this yourself and use it in your projects ?, or any feedback
And if you want more tips, projects like this, consider a follow, more stuff is coming soon...
Top comments (0)