DEV Community

Cover image for Create Your Effector-like State Manager ☄️
Orkhan Jafarov
Orkhan Jafarov

Posted on • Updated on

Create Your Effector-like State Manager ☄️

Introduction

We have a lot of state managers in our Javascript-World and use them every day, but now it's a time to understand "How they work".

There's a very nice state manager Effector.js which is very simple to use in your application, also it's easy to replace your current global state manager. So, I recommend to use it for your project and doesn't matter for which framework. I use it on my React Apps.

Let's start to create own Effector-like state manager!

We're gonna create basics looks like Effector, of course Effector is deeper and complex and our state manager is just simple version of it.

1) Firstly, let's create a js file (effector-clone.js) with our state manager. We start with createStore function that create our store instance with events.

export const createStore = initState => {
  let state = initState; // State of instance
    let events = new Map(); // Events subscribed to the current store
    let watchers = []; // Array of watcher that get called on the state changes

  let store = {
    getState: () => state, // Get current state of the store
        on(event, cb) {}, // Method to subscribe event
        dispatch(event, payload) {}, // Dispatch event to make changes in the store
        watch(cb) {} // Subscribe to the state changes
  };

  return store;
};
Enter fullscreen mode Exit fullscreen mode

2) We need to add a function that creates event instance.

Let's add this code into the file above!

export const createEvent = () => {
    // Return function that iterates stores linked to the event
  let event = payload => {
    event.stores.forEach((store) => {
      store.dispatch(event, payload);
    });
  };

    // Let's link stores to the event
    // We're gonna call stores' dispatches on event's call
  event.stores = [];

  return event;
};
Enter fullscreen mode Exit fullscreen mode

3) Implement on, dispatch and watch methods for the store instance.

export const createStore = initState => {
  let state = initState;
    let events = new Map();
    let watchers = [];

  let store = {
    getState: () => state,
        on(event, cb) {
            // Subscribe to store
            // We use event instance as key for map and callback as a value
            // [event: Function]: callback: Function

            // Set event in the events map if it hasn't the event in this store
            if (!events.has(event)) {
        events.set(event, cb);
                event.stores.push(this);
      }
      return this;
        },
        dispatch(event, payload) {
            // We get and call event's callback and
            // set it's result to the store's state

            const cb = events.get(event);
      if (cb && typeof cb === "function") {
        let newState = cb(state, payload);

                // Check if state is the same
        if (newState !== state) {
          state = newState;
        }
      }

            // Iterable callbacks on the state changes
            watchers.forEach((watch) => watch(state, payload));
        },
        watch(cb) {
      watchers.push(cb);
            // Return function to unsubscribe the watcher
      return () => {
        watchers = watchers.filter((i) => i !== cb);
      };
    }
  };

  return store;
};
Enter fullscreen mode Exit fullscreen mode

Core part of our state manager is Done! ✅

Use it with React + hooks ⚛︎

We're gonna use it as a global state manager. It's also okay to use inside your component.

1) Create useStore.js file and add this simple code.

import { useEffect, useState } from "react";

export const useStore = store => {
    // We get initial state of the store
  const [state, setState] = useState(store.getState());

  useEffect(() => {
        // Pass setState function as a callback
        // store.watch() returns unsubscribe function
    const unsubscribe = store.watch(setState);

    return () => {
            // Unsubscribe our watcher on component unmount
      unsubscribe();
    };
  }, [store]);

  return state;
};
Enter fullscreen mode Exit fullscreen mode

2) Create counterStore.js file with our counter store

import { createStore, createEvent } from "./effector-clone";

export const $counter = createStore(0);

export const inc = createEvent();
export const dec = createEvent();
export const reset = createEvent();

$counter
  .on(inc, (state) => state + 1)
  .on(dec, (state) => state - 1)
  .on(reset, () => 0);
Enter fullscreen mode Exit fullscreen mode

3) Create a Counter.jsx component

import React from "react";
import { $counter, inc, dec, reset } from "./counterStore";
import { useStore } from "./useStore";

export const Counter = () => {
  const total = useStore($counter);

  return (
    <>
      <p>Total: <b>{total}</b></p>
      <button onClick={dec}>-</button>
      <button onClick={reset}>Reset</button>
      <button onClick={inc}>+</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Magic works ✨✨✨

Conclusion

We created own custom state manager and its size with useStore hook is only 1.4kb. I hope now it's a bit cleaner about how it works and how to create own state manager. Of course it needs upgrades and error handlers, but depends on your feedbacks I'll publish an article about these upgrades.

Try it on codesandbox! 🔥

Thank You For Reading!


by Orkhan Jafarov

Top comments (6)

Collapse
 
trashhalo profile image
Stephen Solka

Didnt read. <3 purely for sick header image. A+

Collapse
 
zerobias profile image
Dmitry

Cool post! 👍 Do you plan to make an article about making library methods like combine?

Collapse
 
orkhanjafarovr profile image
Orkhan Jafarov

Thanks mate! I have plan about upgrading it. I'll think about the combine method 👍 and other similar methods. (createEffect, createApi and etc)

Collapse
 
rah1m profile image
Rahim

Thank you!!

Collapse
 
calag4n profile image
calag4n

Nice post 👍.
Isn't useReducer hook simpler to use to achieve the same goal ?

Collapse
 
orkhanjafarovr profile image
Orkhan Jafarov • Edited

Thanks! redux-like managers have headache "a lot of" boilerplates before implement something. This kinda code (effector for example) has less code and cleaner logic