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.
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])
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:
store.get()
– this method is used to retrieve the current data in the state.store.on(event, callback)
– this method is used to register an event listener to a specified event name.store.dispatch(event, data)
– this method is used to emit events passed in with optional data as required by the event defined.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:
@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.@dispatch
– this event is fired on every new action. It is useful for debugging.@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.
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.
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:
deleteNote(id)
– this method dispatches the deleteNote
event when triggered.submit()
– this method dispatches the addNote
event by passing the value of the input state, which is defined locally in our Notes
component.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); }
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:
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:
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Event-driven state management in React using Storeon"
Why not just use Redux?
This is excellent! It brings the good tooling of Redux plus object orientation for creating business objects (in each store) of MobX