Share Data between Riot Components with Riot-Mieosis (State Manager)

Steeve - Apr 21 - - Dev Community

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.

These articles form a series focusing on RiotJS paired with BeerCSS, 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) 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.
  • 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. For RiotJS, the state manager is riot-meiosis.

Originally, Meiosis 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 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

Riot Mieosis Setup

First install the NPM package into your Riot/Vite project:

npm i --save @riot-tools/meiosis
Enter fullscreen mode Exit fullscreen mode

Then create a store.js file, it will be the global state:


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
};
Enter fullscreen mode Exit fullscreen mode

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 a component and a merge function 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) in c-welcome-card.riot, and connect the store to the component:

<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>
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/meiosis/c-form.riot

Code details:

  • Three custom components are loaded: an input, a dialog, and a button.
  • 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:

<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>
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .