How to manage state in React with Eventrix

In this article, I will show you how to easily manage a global state using Eventrix. I will write a “to-do app” as an example application. You can find the full application example here.

Installation

Of course, as with any library, you need to install it and write a few lines of code to be able to use it.

npm install eventrix — save

Project configuration

After installation, create a file in which we’ll initiate the Eventrix instance.

import { Eventrix } from "eventrix";
import taskEventsReceivers from "./tasks";

const initialState = {
    tasks: []
};

export default new Eventrix(initialState, taskEventsReceivers);

Eventrix takes the initial state of our application as the first argument and the list of receivers as the second argument. I’ll explain what a receiver is and how to use it later on.

As we have already defined the instance and created the default global state of our application, the last step is to use Provider. It is responsible for passing the context of our Eventrix instance to all components where it will be needed.

import React from "react";
import { EventrixProvider } from "eventrix";

import eventrix from "./eventrix";
import TodoList from "./TodoList";
import CreateTaskForm from "./CreateTaskForm";
import TodoFooter from "./TodoFooter";
import "./styles.css";

export default function App() {
    return (
        <EventrixProvider eventrix={eventrix}>
            <div className="App">
                <h1>TO DO LIST</h1>
                <div className="todo-app">
                    <CreateTaskForm />
                    <TodoList />
                    <TodoFooter />
                </div>
            </div>
        </EventrixProvider>
    );
}

Provider works pretty much the same way as in other state management libraries. It takes props from an Eventrix instance that will be used in application components.

Use of state in a list component

Now that we have everything configured, we can start using the state in our components, for this we use the useEventrixState Hook. As an example, let’s create a task list and a filter.

import React from "react";
import { useEventrixState } from "eventrix";

import Task from "./Task";
import "./TodoList.css";

const TodoList = () => {
    const [tasks] = useEventrixState("tasks");
    const [statusFilter, setStatusFilter] = useEventrixState("filter.status");
    
    const tasksList = statusFilter ? tasks.filter((task) => task.status === statusFilter) : tasks;
    
    return (
        <div>
            <div className="filters">
                <div
                    className={`status-button ${!statusFilter ? "active" : ""}`}
                    onClick={() => setStatusFilter("")}
                >
                    All
                </div>
                <div
                    className={`status-button ${statusFilter === "todo" ? "active" : ""}`}
                    onClick={() => setStatusFilter("todo")}
                >
                    Todo
                </div>
                <div
                    className={`status-button ${statusFilter === "done" ? "active" : ""}`}
                    onClick={() => setStatusFilter("done")}
                >
                    Done
                </div>
            </div>
            <div className="list">
                {tasksList.length === 0 && (
                    <div className="list-placeholder">Tasks list empty</div>
                )}
                {tasksList.map((task) => (
                    <Task key={task.id} task={task} />
                ))}
            </div>
        </div>
    );
};

export default TodoList;

Hook takes as the first attribute the path to the state of interest. If the state does not exist, it is okay, we will then get “undefined” and if we set some state, the path will be automatically created. Hook also causes automatic re-rendering of the component if the value of the state indicated by us changes. It is very important to give the path exactly to the value we are going to use. Eventrix uses accurate paths to analyze which components will be reloaded when changing the state.

As you can see, we’ve added “filters.status” path to the “status” filter. This means that we take the “status” value from the “filters” object in the state.

const [
statusFilter,
setStatusFilter
] = useEventrixState(“filter.status”);

By specifying the exact path, we guarantee that the component will only reload when someone modifies the entire “filter” object or changes the value of “filter.status” in the object. If someone changes a different value of a filter, the component will not be reloaded because these changes are not important for this component. Hook works similarly to useState but only uses the global state and does not default to the initial state value, which is defined in the initial state of the application. If you don’t set an initial value then you can use the default value.

Calling up a task adding event

Eventrix, instead of operating on actions, operates on its own events. Components and other elements of the application have the ability to emit and listen to events.

Let’s implement the task adding form where we will need to emit an event in order to add a task to the list using the useEmit hook.

Hook takes the name of the event that will be emitted as the first attribute. The data to be delivered with the event is transferred as the second attribute. This event will be delivered to all elements that are listening to it. We currently do not have any registered items using the “CREATE_TASK_EVENT_NAME” event yet. Of course, to be precise, the name of the event is a string.

const CREATE_TASK_EVENT_NAME = ‘tasks:create’;

Receiving events and set value to the state

If we want to receive an event and modify the state based on it, we use EventsReceiver. It is responsible for receiving events and modifying the state of the application synchronously or asynchronously. So let’s create a receiver that will be responsible for adding a task to the task list.

import { EventsReceiver } from "eventrix";

export const CREATE_TASK_EVENT_NAME = "tasks:create";

const createTaskReceiver = new EventsReceiver(
    CREATE_TASK_EVENT_NAME,
    (eventName, { task }, stateManager) => {
        const tasks = stateManager.getState("tasks");
        stateManager.setState("tasks", [task, ...tasks]);
    }
);

export default [
    createTaskReceiver,
];

EventsReceiver takes the name of the event or an array of event names to which it will be listening as the first argument. The second argument is the event handler, i.e. the function that will handle the event. In the attributes, this function will receive the name of the event, the data that is sent with the event and the stateManager. StateManager is responsible for state management in the application. You can use it to get and set any state. Of course, we can also specify the state path as in the useEventrixState hook. We provide the path in the case of getting (getState) and setting (setState) the state value.

As I mentioned before, you can modify the state of the application synchronously and asynchronously. The example above shows how this can be done synchronously. If we want to make a request to the API asynchronously and then add the task to the list, we can do it as follows.

import { EventsReceiver } from "eventrix";
import axios from “axios”;

export const CREATE_TASK_EVENT_NAME = "tasks:create";

const createTaskReceiver = new EventsReceiver(
    CREATE_TASK_EVENT_NAME,
    (eventName, { task }, stateManager) => {
        return axios.post('/tasks', { task }).then(({ data }) => {
            const tasks = stateManager.getState('tasks');
            stateManager.setState('tasks', [data, ...tasks]);
        });    
    }
);

export default [
    createTaskReceiver,
];

Adding support for other events

We already have a form and a to-do list. Now we are going to add other events that will allow us to remove tasks and change their status. For this purpose, we create a “Task” component that will emit events.

import React from "react";
import { useEmit } from "eventrix";
import {
    MARK_TASK_AS_DONE_EVENT_NAME,
    MARK_TASK_AS_TODO_EVENT_NAME,
    REMOVE_TASK_EVENT_NAME
} from "./eventrix/tasks";

const Task = ({ task }) => {
    const emit = useEmit();
    return (
        <div className="list-item">
            <input
                type="checkbox"
                checked={task.status === "done"}
                onChange={() =>
                    emit(
                        task.status === "todo"
                            ? MARK_TASK_AS_DONE_EVENT_NAME
                            : MARK_TASK_AS_TODO_EVENT_NAME,
                        { id: task.id }
                    )
                }
            />
            <div className="task-title">{task.title}</div>
            <div className={`status ${task.status}`}>{task.status}</div>
            <button
                onClick={() => emit(REMOVE_TASK_EVENT_NAME, { id: task.id })}
                className="removeButton"
            >
                X
            </button>
        </div>
    );
};

export default Task;

The task component emits all the events we need. We can now add receivers that will modify the state of our application.

import findIndex from "lodash/findIndex";
import { EventsReceiver } from "eventrix";

export const CREATE_TASK_EVENT_NAME = "tasks:create";
export const REMOVE_TASK_EVENT_NAME = "tasks:remove";
export const MARK_TASK_AS_DONE_EVENT_NAME = "tasks:markAsDone";
export const MARK_TASK_AS_TODO_EVENT_NAME = "tasks:markAsTodo";

const createTaskReceiver = new EventsReceiver(
    CREATE_TASK_EVENT_NAME,
    (eventName, { task }, stateManager) => {
        const tasks = stateManager.getState("tasks");
        stateManager.setState("tasks", [task, ...tasks]);
    }
);

const removeTaskReceiver = new EventsReceiver(
    REMOVE_TASK_EVENT_NAME,
    (eventName, { id }, stateManager) => {
        const tasks = stateManager.getState("tasks");
        const newTasks = tasks.filter((task) => task.id !== id);
        stateManager.setState("tasks", newTasks);
    }
);

const markAsDoneReceiver = new EventsReceiver(
    MARK_TASK_AS_DONE_EVENT_NAME,
    (eventName, { id }, stateManager) => {
        const tasks = stateManager.getState("tasks");
        const taskIndex = findIndex(tasks, (task) => task.id === id);
        const task = { ...tasks[taskIndex], status: "done" };
        stateManager.setState(`tasks.${taskIndex}`, task);
    }
);

const markAsTodoReceiver = new EventsReceiver(
    MARK_TASK_AS_TODO_EVENT_NAME,
    (eventName, { id }, stateManager) => {
        const tasks = stateManager.getState("tasks");
        const taskIndex = findIndex(tasks, (task) => task.id === id);
        const task = { ...tasks[taskIndex], status: "todo" };
        stateManager.setState(`tasks.${taskIndex}`, task);
    }
);

export default [
    markAsDoneReceiver,
    markAsTodoReceiver,
    createTaskReceiver,
    removeTaskReceiver
];

The receivers that we have added are transferred to the Eventrix instance when it is created (project configuration step). We can also add receivers dynamically while the application is running using the eventrix.useReceiver function and remove it using eventrix.removeReceiver.

Architecture of Eventrix

Eventrix library architecture state management react

Devtools

Eventrix also has development tools with the following functionalities:

  • preview state
  • view the history of state changes
  • event history preview
  • view statistics of the number of items listening per each state
  • view statistics of the number of state changes
  • view statistics of the number of events emitted

These tools significantly facilitate application optimization and condition analysis. So far, these tools are available for the Chrome browser here.

Conclusion

Eventrix is ​​a state management library that doesn’t require us to write a lot of code. The big plus is efficiency, flexibility and scalability. What I mean here is the fact that the number of elements connected to Eventrix or states does not affect the performance of the application. Eventrix, apart from condition management, gives us the ability to communicate between components through events. It is a kind of message broker on the frontend.

The library is new, and it does not have a large community yet, but it has RST Software behind it, a software development company that helps both startups and large enterprises to build their applications based on micro-services (such as, among other, Trans.eu), which guarantees it will be maintained and developed. I really recommend you give it a try in one of your projects and feel free to submit your feedback either in the comments below or on our GitHub.