Share Data between Riot Components with Riot-Mieosis (State Manager)
Tags: riotjs, beginners, frontend, javascript
Published: 2024-04-21 19:17:35

This article covers the creation of a state manager, to share data between multiple RiotJS Components

Before starting, make sure you have a base application running, or read my previous article Setup [Riot + BeerCSS + Vite](https://dev.to/steeve/riotjs-material-design-how-to-setup-beercss-269c).

> These articles form a series focusing on [RiotJS](https://riot.js.org/) paired with [BeerCSS](https://www.beercss.com/), designed to guide you through creating components and mastering best practices for building production-ready applications. I assume you have a foundational understanding of Riot; however, feel free to refer to the documentation if needed: https://riot.js.org/documentation/

It exists three methods for sharing data between components:
* Use [Riot properties (props)](https://riot.js.org/documentation/#properties) to pass values to a child component. The child component must emit Events to the parent component if an action happens, such as a click or input change. **In this case, the communication scope is limited:** parent component to child and child to parent.
* Use an Event Emitter like Mitt, a messaging pattern called Pub/Sub, [learn more on my previous article](https://dev.to/steeve/share-data-between-riot-components-with-mitt-event-emitter-58k4).
* **Last method is a state manager:** A global state shared and accessible by all components. In other frontend frameworks, you may have heard about Pinia for Vuejs or Redux for React. <b>For RiotJS, the state manager is [riot-meiosis](https://github.com/riot-tools/meiosis#readme).</b>

Originally, [Meiosis](https://meiosis.js.org) is a state manager pattern using Streams to communicate values, with the principle to make the usage really simple. Quote from the Meiosis documentation:

> The idea is to have a single, top-level object that represents the state of your application, and having a straightforward way to update that state. Views are rendered according to the state, and trigger actions to update the state. That's it!

[Riot-meiosis](https://github.com/riot-tools/meiosis#readme) reproduces the pattern and is dedicated to RiotJS. This article shows how to use it, the goal is to:
* **Step 1:** Create a global state manager
* **Step 2:** On the home page of the Riot app, display a card welcoming a person with their first and last names. 
* **Step 3:** Show a dialog to edit the firstname and lastname.
* **Step 4:** Save new values into the global state

![Riot application using a Global state manager, two components are sharing data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yqqythbdk1e6izl6uv1.gif)

## Riot Mieosis Setup

First install the NPM package into your Riot/Vite project:
```bash
npm i --save @riot-tools/meiosis
```
Then create a **store.js** file, it will be the global state:
```js

import { RiotMeiosis } from '@riot-tools/meiosis';

const state = {
  firstname     : 'Jon',
  lastname      : 'Snow',
  firstnameEdit : '',
  lastnameEdit  : '',
  displayForm   : false
}

/** Create the state manager instance */
const stateManager = new RiotMeiosis(state, { flushOnRead: false, statesToKeep: 1 });

/** Extract the state stream **/
const { stream } = stateManager;

/** Add Root state reducer: merge the old and new state objects */
stream.addReducer((newState, oldState) => {
    // Object oldState { firstname: "John", lastname: "Wick", firstnameEdit: "", lastnameEdit: "", email: "", displayForm: false } 
    // Object newState { displayForm: true }
    return {
        ...oldState,
        ...newState
    }
});

export default { 
    /** Simplifying the connect function for components */
    connect: function (component) {
        return stateManager.connect((globalState, ownState) => ({ ...ownState, ...globalState }))(component)
    },
    /** Provides the dispatch function to update values */
    dispatch: stateManager.dispatch
};
```
> Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/meiosis/store.js

Code details:
* A **reducer** is created to merge the global state with new values coming from all components.
* To connect the component to the global state, the Riot component must be wrapped with the **store.connect()** function, and pass <u>a component</u> and a <u>merge function</u> as an argument: it will merge the component state with the global state. For each component, The global state is accessible  with the expression `this.state.value`.
* The store returns the **connect**, and the **dispatch** function used to update values.
* The global state is initialised with a `first and last name`, a second `first and last name` for the dialog inputs to edit the profile, and a boolean `displayForm` to show the dialog.


Write the following HTML code (found on the [BeerCSS documentation](https://www.beercss.com/#cards)) in **c-welcome-card.riot**, and connect the store to the component:
```html
<c-welcome-card>
    <article class="no-padding border round">
        <img class="responsive small top-round" src="./examples/data/img-card.png">
        <div class="padding">
            <h5>Welcome</h5>
            <p>Bonjour <b>{ state.firstname} { state.lastname }</b> 👋 to our app! We're excited to have you here.Whether you're in finance, marketing, or operations, our app delivers the insights you need to drive growth and stay ahead of the competition.</p>
            <nav>
                <button onclick={ editProfile }>Edit Profile</button>
            </nav>
        </div>
    </article>
    <script>
        import store from "./store.js";

        export default store.connect({
            editProfile () {
                store.dispatch({ 
                    displayForm: true,
                    firstnameEdit: this.state.firstname,
                    lastnameEdit: this.state.lastname,
                })
            }
        })
        
    </script>
</c-welcome-card>
```
> Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/meiosis/c-welcome-card.riot

Code breakdown:
* The store is loaded, and the component is connected with **store.connect()**
* To update one of the values of the global store, call the **store.dispatch()** function.
* When the button is clicked, the function `editProfile()` is fired to show the Dialog, and set the `firstnameEdit` and `lastnameEdit`.

Create a file named **c-form.riot**, and connect the store to access the `firstnameEdit` and `lastnameEdit` values. It will but used for each inputs:
```html
<c-form>
    <c-dialog active={  state.displayForm } oncancel={ close } onconfirm={ confirmEdit }>
        <template slot="body">
            <h5 style="margin-bottom:30px">Edit Profile</h5>
            <c-input value={ state.firstnameEdit } onkeyup={ (ev) => updateInput(ev, 'firstnameEdit') } onchange={ (ev) => updateInput(ev, 'firstnameEdit') } outlined="true" round="true" placeholder="Firstname" />
            <c-input value={ state.lastnameEdit } onkeyup={ (ev) => updateInput(ev, 'lastnameEdit') } onchange={ (ev) => updateInput(ev, 'lastnameEdit') }  outlined="true" round="true" placeholder="Lastname"  />
        </template>
    </c-dialog>
    <script>
        /** State manager **/
        import store from './store.js';
        /** Components **/
        import cInput from '../../components/c-input.riot';
        import cButton from '../../components/c-button.riot';
        import cDialog from '../../components/c-dialog.riot';

        export default store.connect({
            components: {
                cInput,
                cButton,
                cDialog
            },
            close() {
                store.dispatch({ displayForm: false })
            },
            updateInput(ev, keyName) {
                store.dispatch({ [keyName]: ev?.target?.value ?? '' })
            },
            confirmEdit() {
                store.dispatch({ 
                    displayForm: false, 
                    firstname: this.state.firstnameEdit,
                    lastname: this.state.lastnameEdit
                })
            }
        });
    </script>
</c-form>
```
> Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/meiosis/c-form.riot

Code details:
* Three custom components are loaded: an [input](https://dev.to/steeve/input-component-with-riotjs-material-design-30c4), a [dialog](https://dev.to/steeve/dialog-component-with-riotjs-material-design-1c40), and a [button](https://dev.to/steeve/button-component-with-riotjs-material-design-174p).
* The dialog displays two input components to update the `firstnameEdit`, and `lastnameEdit`.
* If the `confirm` button is clicked, the new values are saved into the global state `firstname` and `lastname` thanks to the **store.dispatch** function. The welcome card receives the update; the first and last names are refreshed.

> Why using `firstnameEdit/lastnameEdit` and not directly `firstname/lastname` ? When a user input new names, it can cancel anytime the edition without impacting the `firstname/lastname`. The value change takes effects only when a button `confirm` is clicked.

Now we can load both components into a common **index.riot** file:
```html
<index-riot>
    <div style="width:400px;padding:40px;">
        <c-welcome-card />
        <c-form />
    </div>
    <script>
        
        import cForm from "./c-form.riot"
        import cWelcomeCard from "./c-welcome-card.riot"
    
        export default {
            components: {
                cForm,
                cWelcomeCard
            }
        }
    </script>
</index-riot>
```
> Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/meiosis/index.riot

Both component are independents, only communicating through the global state manager.

## Conclusion

A state manager is quite powerful for reading and updating data across Riot Components. The data could be lists, objects, or Maps. The store can be connected to unlimited components for large production front-end applications made with RiotJS!