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 Mieosis Setup
First install the NPM package into your Riot/Vite project:
npm i --save @riot-tools/meiosis
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
};
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 secondfirst and last name
for the dialog inputs to edit the profile, and a booleandisplayForm
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>
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 thefirstnameEdit
andlastnameEdit
.
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>
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
, andlastnameEdit
. - If the
confirm
button is clicked, the new values are saved into the global statefirstname
andlastname
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 directlyfirstname/lastname
? When a user input new names, it can cancel anytime the edition without impacting thefirstname/lastname
. The value change takes effects only when a buttonconfirm
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>
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!