Abdulazeez Abdulazeez Adeshina Software enthusiast, writer, food lover, and hacker.

Event-driven state management in React using Storeon

6 min read 1851

Event-driven State Management In React Using Storeon

React’s Context API and Hooks libraries have been used together in the management of application state since the introduction of Hooks. However, the combination of the Context API and Hooks — the foundation on which many Hooks-based state management libraries are built — can be inefficient for large-scale applications.

This combo can also become tiring for developers since they have to create a custom Hook to enable access to the state and its methods before using it in a component. This defeats the true purpose of Hooks: simplicity. Yet for smaller apps, Redux can be overkill.

So today, we’ll be discussing an alternative that uses the Context API: Storeon. Storeon is a tiny, event-driven React state management library with principles similar to Redux. The state actions can be seen and visualized with the Redux DevTools. Storeon uses the Context API internally to manage state and employs an event-driven approach for state operations.

Stores

A store is a collection of data stored in an application’s state. It is created with the createStoreon() function imported from the Storeon library.

The createStoreon() function accepts a list of modules, wherein each module is a function that accepts a store parameter and binds their event listeners. Here’s an example of a store:

import { createStoreon } from 'storeon/react'
// todos module
const todos = store => {
  store.on(event, callback)
}

export default const store = createStoreon([todos])

Modularity

Stores in Storeon are modular — that is, they are independently defined and not bound to a Hook or component. Every state and its operational methods are defined under functions known as modules. These modules are passed into the createStoreon() function to register them as global stores.

The store has three methods:

  1. store.get() – this method is used to retrieve the current data in the state.
  2. store.on(event, callback) – this method is used to register an event listener to a specified event name.
  3. store.dispatch(event, data) – this method is used to emit events passed in with optional data as required by the event defined.

Events

Storeon is an event-based state management library, and as a result, changes to the state are emitted by events defined in the state modules. There are three inbuilt events in Storeon beginning with the @ prefix; other events are to be defined without the @ prefix. The three inbuilt events are:

  1. @init – this event is fired when the application loads. It is used to set the application’s initial state and executes whatever is in the callback passed to it.
  2. @dispatch – this event is fired on every new action. It is useful for debugging.
  3. @changed – this event is fired when there are changes in the application state.

Note: store.on(event, callback) is used to add an event listener in our module.

What we will build

To demonstrate how application state operations are carried out in Storeon, we’ll build a simple notes app. We’ll also be using another package from Storeon to save our state data in localStorage.

From here, I’ll assume you have basic knowledge of JavaScript and React. You can find the code used in this article on GitHub.

We made a custom demo for .
No really. Click here to check it out.

Setup

Before we dive in too deep, let’s map out the project structure and the installation of the dependencies needed for our notes application. We’ll start by creating our project folder.

mkdir storeon-app && cd storeon-app
mkdir {src,public,src/Components}
touch public/{index.html, style.css} && touch src/{index,store,Components/Notes}.js

Next, we initialize the directory and install the dependencies needed.

npm init -y
npm i react react-dom react-scripts storeon @storeon/localstorage uuidv4

Now it’s time to write the parent component in our index.js file.

index.js

This file is responsible for rendering our notes component. First, we’ll import the required packages.

import React from 'react'
import { render } from 'react-dom';

function App() {
  return (
    <>
      Hello!
    </>
  );
}
const root = document.getElementById('root');
render(<App />, root);

Next, we’ll build our application store by writing the code for the initialization and operations of the state in store.js.

store.js

This file is responsible for handling state and subsequent state management operations in our app. We must create a module to store our state alongside its supporting events to handle operational changes.

We’ll start by importing the createStoreon method from Storeon and the unique random ID generator UUID.

The createStoreon method is responsible for the registration of our state into a global store.

import { createStoreon } from 'storeon';
import { v4 as uuidv4 } from 'uuid'
import { persistState } from '@storeon/localstorage';

let note = store => {}

We’ll be storing our state in an array variable notes, which will contain notes in the following format:

{
  id: 'note id',
  item: 'note item'
},

Next, we’ll populate the note module first by initializing the state with two notes that’ll be displayed when we start our app for the first time. Then we’ll define the state events.

let note = store => {
  store.on('@init', () => ({
    notes: [
      { id: uuidv4(), item: 'Storeon is a React state management library and unlike other state management libraries that use Context, it utilizes an event-driven approach like Redux.' },
      { id: uuidv4(), item: 'This is a really short note. I have begun to study the basic concepts of technical writing and I'\'m optimistic about becoming one of the best technical writers.' },
    ]
  });
  store.on('addNote', ({ notes }, note) => {
    return {
      notes: [...notes, { id: uuidv4(), item: note }],
    }
  });
  store.on('deleteNote', ({ notes }, id) => ({
    notes: notes.filter(note => note.id !== id),
  });
}

In the code block above, we defined the state and populated it with two short notes and defined two events and callback functions to be executed once the event is emitted from the dispatch(event, data) function.

In the addNote event, we return an updated state object with the new note added, and in the deleteNote event, we filter out the notes whose IDs were passed to the dispatch method.

Finally, we register the module as a global store by assigning it to an exportable variable store so we can import it to our context provider later and store the state in localStorage.

const store = createStoreon([
  notes,
  // Store state in localStorage
  persistState(['notes']),
]);
export default store;

Next, we’ll write our notes app component in Notes.js.

Notes.js

This file houses the component for our notes app. We’ll start by importing our dependencies.

import React from 'react';
import { useStoreon } from 'storeon/react';

Next, we’ll write our component.

const Notes = () => {
  const { dispatch, notes } = useStoreon('notes');
  const [ value, setValue ] = React.useState(''); 
}

On the second line in the code block above, the return values from the useStoreon() Hook are set to a destructible object. The useStoreon() Hook takes the module name as its argument and returns the state and a dispatch method to emit events.

Next, we’ll define methods to emit our state-defined events in our component.

const Notes = () => {
...
  const deleteNote = id => {
    dispatch('deleteNote', id)
  };
  const submit = () => {
    dispatch('addNote', value);
    setValue('');
  };
  const handleInput = e => {
    setValue(e.target.value);
  };
}

Let’s review the three methods we defined the above:

  1. deleteNote(id) – this method dispatches the deleteNote event when triggered.
  2. submit() – this method dispatches the addNote event by passing the value of the input state, which is defined locally in our Notes component.
  3. handleInput() – this method sets the value of the local state to the user input.

Next, we’ll build the main interface of our app and export it.

const Notes = () => {
  ...
  return (
    <section>
      <header>Quick Notes</header>

      <div className='addNote'>
        <textarea onChange={handleInput} value={value} />
        <button onClick={() => submit()}> Add A Note </button>
      </div>

      <ul>
        {notes.map(note => (
          <li key={note.id}>
            <div className='todo'>
              <p>{note.item}</p>
              <button onClick={() => deleteNote(note.id)}>Delete note</button>
            </div>
          </li>
        ))}
      </ul>
    </section>
  );
}

And that wraps up our Notes component. Next, we’ll write the stylesheet for our app and the index.html file.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="style.css">
    <title>Storeon Todo App</title>
</head>

<body>
    <div id="root"></div>
</body>

</html>

Next, we’ll populate our style.css file.

style.css

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

section {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 300px;
  margin: auto;
}

header {
  text-align: center;
  font-size: 24px;
  line-height: 40px;
}

ul {
  display: block;
}

.todo {
  display: block;
  margin: 12px 0;
  width: 300px;
  padding: 16px;
  box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.3);
  transition: 0.2s;
  word-break: break-word;
}

li {
  list-style-type: none;
  display: block;
}

textarea {
  border: 1px double;
  box-shadow: 1px 1px 1px #999;
  height: 100px;
  margin: 12px 0;
  width: 100%;
  padding: 5px 10px;
}

button {
  margin: 8px 0;
  border-radius: 5px;
  padding: 10px 25px;
}

.box:hover {
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}

Running our app

Now that we have successfully written our components and stylesheet, we haven’t updated our parent component in index.js to render our notes component. Let’s render our notes component.

index.js

To access our global store, we will have to import our store and the Storeon store context component. We will also import our notes component to render it.

Replace the content of the component with this:

import React from 'react';
import { render } from 'react-dom';
import { StoreContext } from 'storeon/react';
import Notes from './Components/Notes';
import store from '../src/store';

function App() {
  return (
    <>
      <StoreContext.Provider value={store}>
        <Notes />
      </StoreContext.Provider>
    </>
  );
}

const root = document.getElementById('root');
render(<App />, root);

On lines 8–10, we call the store context provider component and pass the notes component as the consumer. The store context provider component takes the global store as its context value.

Next, we’ll edit the scripts section in the package.json file to the below:

"scripts": {
  "start": "react-scripts start",
}

Then we run our app:

npm run start

Let’s go on to add and delete notes:

Adding And Deleting Notes In Our App

Storeon devtools

Storeon shares similar attributes with Redux, and as a result, state changes can be visualized and monitored in the Redux DevTools. To visualize the state in our Storeon app, we will import the devtools package and add it as a parameter to the createStoreon() method in our store.js file.

...
import { storeonDevtools } from 'storeon/devtools';
...
const store = createStoreon([
  ...,
  process.env.NODE_ENV !== 'production' && storeonDevtools,
]);

Here’s a demonstration using the Redux DevTools to visualize state changes:

Using Redux DevTools To Visualize State Changes In Our App

Conclusion

This tutorial should give you a basic understanding of what Storeon is all about and how it works. The main takeaway is that you don’t have to write a custom Hook and make container variables to be able to us state in your components.

Storeon is a very useful state management library that uses the event-driven approach and modular style adapted from Redux to manage state. Again, you can find the code used in this article on GitHub.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Abdulazeez Abdulazeez Adeshina Software enthusiast, writer, food lover, and hacker.

2 Replies to “Event-driven state management in React using Storeon”

Leave a Reply