DEV Community

Cover image for My thoughts on: Mithril
Sho Carter-Daniel
Sho Carter-Daniel

Posted on • Updated on

My thoughts on: Mithril

Gone were the days when we would add a small <script></script> into our HTML to include a library into our projects. Now we have bundlers and transpilers (like Webpack and Babel), and now we have other frontend build tools like Snowpack. Finally, we have the ability to import modules into our browsers (except IE).

JQuery was the UI library of choice, but now we have Angular, React and Vue. There are a few others I have had the privilege to work, namely Lit-Element AureliaJS, and MithrilJS.


Now, moving on...

MithrilJS isn't a UI Library alone (like React or Vue). It is an actual framework that "provides routing and XHR utilities out of the box" as it says on their website.

It is fast, super light and leverages on the power of VirtualDOM. The community is vibrant and energetic too with quick responses to questions etc. The API is relatively small and you would be surprised at what you can get done within a small amount of time with this framework. If you love the idea of working with plain JavaScript objects without having to worry about learning some form of new template syntax, look no further. There may be the odd occasion where you (or your team) may need to deliver a small-medium sized project in a short space of time without the additional bloat (and learning curve) that other frameworks may bring. Mithril (in my opinion) would be a viable option in that respect.


Installation

Installation is pretty simple. Just copy and paste this link into your web app.

<script src="https://unpkg.com/mithril/mithril.js"></script>
Enter fullscreen mode Exit fullscreen mode

...or via NPM, run the following command in your terminal.

$ npm install mithril --save
Enter fullscreen mode Exit fullscreen mode

If you would like to integrate it to your TypeScript project, you can do so by simply running the following command in your terminal.

$ npm install mithril --save
$ npm install @types/mithril --save-dev
Enter fullscreen mode Exit fullscreen mode

If you would like to use Mithril with JSX, follow the instructions here.


Mithril Components

There are three ways to create a component in Mithril.

// Function
function Component() {
    let type = 'Functional component...'

    return {
        view() {
            return m('div', `This is the ${type}.`);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
// Class
class Component {
    oninit() {
        this.data = {
            type: 'class component'
        }
    }

    view() {
        return m('div', `This is the ${this.data.type}.`);
    }
}
Enter fullscreen mode Exit fullscreen mode
// Object literal
const Component = {
  data: {
    type: 'object literal component'
  },
  view() {
    return m('div', `This is the ${this.data.type}.`);
  }
};
Enter fullscreen mode Exit fullscreen mode

Mithril Function

The Mithril function is very similar (in its syntax) to React without JSX. You can take a look here.

// A Mithril function can take 2-3 arguments
// m(elementName, textContent)
// m(elementName, attributes, textContent)
import m from 'mithril';

// 2 arguments
const element = m('div', 'Hello world');

// 3 arguments
const attributes = {
  onclick: () => console.log('You have clicked on me!')
};
const elementWithAttributes = m('button', attributes, 'Click me!');


/**
 * To display your component...
 *
 * This is your equivalent in:
 * - React <DummyComponent name={'World'} /> 
 * - Angular <app-dummy-component [name]="'World'"></app-dummy-component>
 * - VueJS <dummy-component .name="'World'" />
 */
const DummyComponent = {
  view(vnode) {
    return m('div', `Hello ${vnode.attrs.name}`);
  }
}
m(DummyComponent, { name: 'world' });

// ...and to nest elements, you would do this
m('div',
  m('p', 'This is a post on MithrilJS.'),
  m('button', 'Click me!'),
);
Enter fullscreen mode Exit fullscreen mode

We can see a common thread in the components. The view method serves as function that returns your mithril element. You can either return m() or your can return an array of m() components.

I personally love to separate my components into two types: "Smart" and "Dumb" components.

const Model = {
  getUsers() {
    return m.request({
      method: "GET",
      url: "https://jsonplaceholder.typicode.com/users"
    });
  }
}

class AppComponent {
  oninit() {
    // declare properties upon component initialization
    this.data = {
      selected: null,
      users: []
    };

    // get the users, and assign them
    Model.getUsers().then(
      users => this.data.users = users,
      err => console.error(err)
    );
  }

  /**
   * React equivalent of:
   * <section>
   *  <UserComponent user={this.state.selected} />
   *  <UserListComponent selected={() => this.onUserSelect()} users={this.state.users} />
   * </section>
   * 
   * Angular equivalent of:
   * <section>
   *  <app-user [user]="selected"></app-user-component>
   *  <app-user-list [users]="users" (selected)="onUserSelect()"></app-user-component>
   * </section>
   */
  view() {
    return m('section',
      m(UserComponent, { user: this.data.selected }),
      m(UserListComponent, {
        selected: user => this.onUserSelect(user),
        users: this.data.users
      }),
    );
  }

  // events can go here

  onUserSelect(user) {
    this.data.selected = user;
  }
}


// dumb components
class UserListComponent {
  // extract the selected and users properties from the "attributes" keys
  view({ attrs: { selected, users } }) {
    return users
      ? users.map(user => m('p', { onclick: () => selected(user) }, `${user.name} (${user.email})`))
      : m('div', 'No users available atm.');
  }
}

class UserComponent {
  view({ attrs: { user } }) {
    return user ? m('div', `You have selected ${user.name}`): '';
  }
}
Enter fullscreen mode Exit fullscreen mode

Lifecycle Hooks

Mithril has its own set of lifecycle hooks. Here they are:

class Component {
  constructor() {
    this.data = {
      name: 'World',
    };
  }

  // "oninit" is run before DOM element is attached 
  oninit(vnode) {}

  // "oncreate" is run after the DOM element is attached
  oncreate(vnode) {}

  // "onbeforeupdate" is run before the DOM element is updated
  onbeforeupdate(newVnode, oldVnode) {}

  // "onupdate" is run when the DOM element is updated whilst attached to the document
  onupdate(vnode) {}

  // "onbeforeremove" is run before the DOM element is detached
  onbeforeremove(vnode) {}

  // "onremove" is when the DOM element has been detached
  onremove(vnode) {}

  view(vnode) {
    return m('div', `Hello ${this.data.name}`);
  }
}

m.mount(document.body, Component);
Enter fullscreen mode Exit fullscreen mode

Routing

Now, for the sake of this post, we will stick with the class approach to developing our components.

// m.route(element, homeUrl, links)
class HomeComponent {
  view() {
    return m('div', `Home component.`);
  }
}

class UserComponent {
  view() {
    return m('div', `User component.`);
  }
}

class ErrorComponent {
  view() {
    return m('div', `There is an error!`);
  }
}

class UserEditComponent {
  view({ attrs }) {
    console.log('"ID" Parameter:', attrs.id);
    return m('div', 'This is the User component to edit.');
  }
}

m.route(document.body, "/", {
  "/": HomeComponent,
  "/users/:id": UserComponent,
  "/users/:id/edit": {
    onmatch: () => {
      // once the URL has been matched
      if (localStorage.getItem('jwt')) {
        return UserEditComponent;
      }

      return ErrorComponent
    },
    render: vnode => {
      // code all you want here before the "vnode" is injected into the component
      const authenticated = randomSource.get('key');
      return [{ ...vnode, authenticated }];
    },
  }
});
Enter fullscreen mode Exit fullscreen mode

Sharing data between components

You can easily share data between two components for example:

const Store = {
  state: { counter: 0 },
  incrementCounter() {
    this.state.counter++;
  },
}

class DisplayComponent {
  view() {
    return m('div', `You have clicked on the button ${Store.state.counter} times.`);
  }
}

class Component {
  view() {
    return [
      // display the DIV element -> that would contain BUTTON element
      m('div',
        m('button', {
          onclick: () => Store.incrementCounter()
        }, `Counter`)
      ),

      // display the DISPLAY component
      m(DisplayComponent),
    ];
  }
}
Enter fullscreen mode Exit fullscreen mode

In conclusion...

The developers of MithrilJS have done a fantastic job in creating a fast and light framework, with a strong API. There are some other things I may not have mentioned at the top, but can be found on the MithrilJS website.

It is:

  • Light (9.5kb)
  • Pretty rapid
  • Simple to integrate in an existing project
  • Backed by a strong vibrant community
  • Helps improve your JavaScript since there is no "template language"
  • Fun to use

If this has helped you, please feel free to leave a comment below. If you have questions, or would disagree, I'm open to discuss.

Top comments (8)

Collapse
 
shadowtime2000 profile image
shadowtime2000

The thing about Mithril is the explicit separation of all those different methods which combined with like object literals just ends up becoming messier React class components.

Collapse
 
sho_carter profile image
Sho Carter-Daniel

Could you please elaborate? Would you be inferring preference of templates (JSX, etc) over plain JavaScript objects?

Thanks.

Collapse
 
shadowtime2000 profile image
shadowtime2000

I don't think you understand what I said. I am talking about with Mithril components you have to either have object literals with functions for every single step into the event loop or you can do it with classes with a method for every single step into the event loop which results in messy code that in my opinion doesn't look that clean compared to say, React functional component like components.

Thread Thread
 
sho_carter profile image
Sho Carter-Daniel • Edited

What do you mean by "functions for every single step into the event loop"?

What do you mean by "event loop"?

Thread Thread
 
shadowtime2000 profile image
shadowtime2000

I mean you have to have a seperate function for every event.

Thread Thread
 
sho_carter profile image
Sho Carter-Daniel • Edited

Okay, I get what you mean. I believe React would have the same problem if you were to use it without JSX no?

Mithril can be used with JSX, with the exception of having to explicitly declare the "view" property if you were to go down the functional component route.


import React, { createElement, useState } from 'react';
import ReactDOM from 'react-dom';

// Function approach
const AppComponent = () => {
    const [name, changeName] = useState('world');

    return createElement('div', {
        onClick: () => changeName('John Doe'),
    }, `Hello ${name}.`);
}

// Class approach
class AppComponent extends React.Component {
    constructor() {
        super();
        this.state = {
            name: 'world',
        };
    }

    render() {
        return React.createElement('div', {
            onClick: () => this.setState({ name: 'John Doe' })
        }, `Hello ${this.state.name}.`);
    }
}

ReactDOM.render(createElement(AppComponent), document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
shadowtime2000 profile image
shadowtime2000

So the problem with Mithril is that it uses a messy class component system. I don't see how JSX is relevant here at all. I am just saying Mithril's component system based off of classes is kind of messy to read and write, while like functional component frameworks are like less messy. Mithril seems to be like React without functional components.

Thread Thread
 
kevinfiol profile image
Kevin Fiol

@sho_carter I think by "methods for every single step in the event loop", @shadowtime2000 is referring to the lifecycle hooks. In Mithril, these are oninit, oncreate, onbeforeupdate, etc. React also has lifecycle hooks, but for class components.

Lifecycle hooks are completely optional, and I rarely use them in my Mithril applications unless integrating a third-party library. You'll find yourself in a similar situation while using React, except these days you're probably using useEffect in place of lifecycle hooks. In which case, if you prefer the ergonomics of React hooks... use Mithril hooks.

Mithril seems to be like React without functional components.

Mithril does have function components, as seen here in Sho's post. :) But from your comments, I've gathered your issue is these function components return object literals with the view method defined. The overhead of defining an object literal component or closure component without lifecycle hooks is, frankly, tiny. The critique that it is messier seems a bit contrived in my opinion:

// Mithril
const Foo = {
    view: () => m('p', 'hello world')
};

// React
function Foo() {
    return <p>hello world</p>
};
Enter fullscreen mode Exit fullscreen mode

Also, if you really want to skip defining any Mithril component methods, then by all means do so. Mithril allows completely stateless, UI components.