DEV Community

Cover image for Reactive Data Structures in MeteorJS - Reactive Stack
Jan Küster for Meteor Software

Posted on

Reactive Data Structures in MeteorJS - Reactive Stack

MeteorJS brings client-side reactivity out of the box. No matter which frontend framework you choose, you will always have an integrated reactivity that synchronizes your data and the UI. This is one of the core strengths of MeteorJS.

But that's not the only advantage. It allows you to write your data structures of any complexity and while you access them using their normal API, reactivity is handled under the hood automatically.

The tutorial is also available as a video:


Goal of this tutorial

In this tutorial I will show you how to write your own reactive data structure using a simple stack implementation.

At the end you will have a fully functional stack, that automatically updates UI and autorun functions when elements are pushed or popped.


Short reminder: What is a stack?

A stack manages data using the list-in-first-out (in short, LIFO) principle. The last item that is pushed onto the stack will be the first item that is accessed when popped from the stack.

Underlying items are inaccessible until they become the top item by popping the stack.

Wikipedia Image of Stack
CC0 wikimedia commons

Let's start coding

1. Create a new project

First of all, make sure MeteorJS is installed on your machine. In case you have no existing MeteorJS project available, create one by opening your terminal and enter:

$ meteor create
Enter fullscreen mode Exit fullscreen mode

Provide a name and the frontend framework you intend to use. for this tutorial, I will use Blaze. However, the reactive stack will work with any of the frontends, available in MeteorJS.

2. Remove boilerplate

In the next step, remove the boilerplate from the client/main.html.
It should look like this now:

<head>
  <title>Meteor App</title>
</head>

<body>
  <h1>Reactive Data Structures</h1>
</body>
Enter fullscreen mode Exit fullscreen mode

Then remove the boilerplate from client/main.js or client/main.jsx if you chose React or Solid. It should look like this now:

import { Template } from 'meteor/templating';
import './main.html';
import './main.css';
Enter fullscreen mode Exit fullscreen mode

3. Initial Stack implementation

Now, create a new file under client/ReactiveStack.js and implement a basic stack. In this first approach, the focus is on the data structure itself and not the reactivity.

export class ReactiveStack {
  constructor () {
    this.items = []
  }

  push (item) {
    this.items.push(item)
    return this.items.length
  }

  pop () {
    const item = this.items.pop()
    return item
  }

  peek () {
    return this.items.at(-1)
  }

  all () {
    return  [].concat(this.items)
  }

  size () {
    return this.items.length
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Integrate the stack into the UI

You can now add a simple template that includes the ReactiveStack. Update your client/main.html file to the following:

<head>
  <title>reactive-datastructures</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <h1>Reactive Data Structures</h1>

  {{> stack }}
</body>


<template name="stack">
  <h2>Stack</h2>

    <!-- user input -->
    <input type="text" name="push" class="item-input">
    <button class="push-btn">Push</button>
    <button class="pop-btn">Pop</button>

    <!-- reactively peek the top item -->
    <div>top item: {{topItem}}</div>

    <div>List:</div>
      <ul>
        {{#each item in items}}
          <li>{{item}}</li>
        {{/each}}
      </ul>
</template>
Enter fullscreen mode Exit fullscreen mode

🤓 A quick explanation about this template

The template renders the stack as a list of items, using an {{#each}} loop. Read the Blaze docs if you don't know how these work.

The top item is delivered by the {{topItem}} helper. The user input is a simple text input and two buttons for push and pop. The topmost item is constantly peeked from the stack.

5. Adding Template logic

The template from the previous step won't work at all, since you have defined only the rendering logic. It doesn't know about a stack, items and topItem. Let's add them by updating client/main.js to the following:

import { Template } from 'meteor/templating';
import {ReactiveStack} from './ReactiveStack'
import './main.html';

Template.stack.onCreated(function () {
  // create a new reactive stack
  const instance = this
  instance.stack = new ReactiveStack()

  // simple demonstration of autorun
  // within templates, based on reactive changes
  instance.autorun(() => {
    const size = instance.stack.size()
    const top = instance.stack.peek()
    console.debug({ size, top })
  })
})


// reactive helpers to use within the template
Template.stack.helpers({
  topItem () {
    return Template.instance().stack.peek()
  },
  items () {
    return Template.instance().stack.all().toReversed()
  }
})

// event listeners for the buttons
Template.stack.events({
  'click .push-btn' (e, instance) {
    e.preventDefault()
    const input = instance.$('.item-input')
    const item = input.val()
    instance.stack.push(item)
    input.val(null) // clear input
  },
  'click .pop-btn' (e, instance) {
    e.preventDefault()
    instance.stack.pop()
  }
})
Enter fullscreen mode Exit fullscreen mode

Try it out by navigating to http://localhost:3000. You will see that there is nothing reactive yet. This will change with the next step.

6. Make it reactive

Let's add some magic. 🧙‍♂️ This is where Tracker comes into play.

Tracker has a simple way to track computations using its Dependency class. While Tracker offers complex functionality to support many use cases, this step focuses solely on two methods this time:

  • changed - to inform about data has changed
  • depend - to inform about data is observed and Tracker is "active" (the method is called within an active computation, e.g. within an autorun).

These two functions are basically responsible for invoking a new computation call each time they are called. In order to make our ReactiveStack truly reactive, we simply add this dependency to it and call changed on any change to items and depend on any read.

Afterwards, the stack looks like this:

import { Tracker } from 'meteor/tracker'

export class ReactiveStack {
  constructor () {
    this.items = []
    this.dep = new Tracker.Dependency()
  }

  push (item) {
    this.items.push(item)
    this.dep.changed()
    return this.items.length
  }

  pop () {
    const item = this.items.pop()
    this.dep.changed()
    return item
  }

  peek () {
    if (Tracker.active) this.dep.depend()
    return this.items.at(-1)
  }

  all () {
    if (Tracker.active) this.dep.depend()
    return  [].concat(this.items)
  }

  size () {
    if (Tracker.active) this.dep.depend()
    return this.items.length
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, navigate back to http://localhost:3000 and try again. At this point, the template will automagically update.

This is because in Blaze, every Template.helper is actually a wrapped autorun computation that invokes a re-render of the attached DOM elements.
Updating the stack invokes the surrounded autorun which itself invokes a rerender of the attached Blaze view with the updated data.

Congratulations, you have implemented a ReactiveStack. 🎉

Where to go from here?

Keep looking (aka follow me) for upcoming articles on more Reactive Data Structures in MeteorJS. Alim, StoryTeller and I also covered the whole topic of reactivity (with focus on signals) in episode 62 of our weekly live stream "This week in MeteorJS".

If you're interested in getting in touch with the community, then join the MeteorJS forums and become a member today.


About me 👋

I regularly publish articles about MeteorJS and JavaScript here on dev.to. I also recently co-hosted the weekly MeteorJS Community Podcast, which covers the latest in Meteor and the community.

You can also find me (and contact me) on GitHub, Twitter/X and LinkedIn.

If you like what you read and want to support me, you can sponsor me on GitHub or buy me a book from my Amazon wishlist.

Top comments (0)