byte by byte

Dojo Widget Middleware

Rene Rubalcava | September 4, 2019

The newest features of Dojo 6 include the new function based widgets and widget middleware.

Dojo class based widgets come with decorators to watch for property changes and work with metas which allow you to get information about your widget.

With the introduction of function based widgets, those patterns have been replaced by the new middleware system.

Manage local state

There are two middlewares available for managing local state in a widget.

  • cache - persists data
  • icache - works like cache, but also invalidates the widget when data changes.

cache

You might use cache for some fine grained state management, because if you do use it, it's up to you to manually invalidate the widget, so that it will render based with updated cache properties using the invalidator middleware.

// src/widgets/Parrot/Parrot.tsx
import { create, invalidator, tsx } from "@dojo/framework/core/vdom";
import cache from "@dojo/framework/core/middleware/cache";

import * as css from "./Parrot.m.css";

// use `cache` and `invalidator` as middleware
// in render factory
const factory = create({ cache, invalidator });

export const Parrot = factory(function Parrot({
  middleware: { cache, invalidator }
}) {
  const name = cache.get<string>("name") || "";
  return (
    <virtual>
      <h3 classes={[css.root]}>{`Polly: ${name}`}</h3>
      <input
        classes={[css.input]}
        placeholder="Polly want a cracker?"
        type="text"
        onkeyup={event => {
          // update cache data with input value
          cache.set(
            "name",
            (event.target as HTMLInputElement).value
          );
          // invalidate widget to render
          // with new data
          invalidator();
        }}
      />
    </virtual>
  );
});

export default Parrot;

You can see this demo in action here.

This is fine, but it could be easier.

icache

The icache is designed specifically to work like cache, but to also run an invalidator() on each update. It also comes with an extra method, icache.getOrSet() that will return the current value or a specified default value if none available.

// src/widgets/Parrot/Parrot.tsx
import { create, tsx } from "@dojo/framework/core/vdom";
import icache from "@dojo/framework/core/middleware/icache";

import * as css from "./Parrot.m.css";

const factory = create({ icache });

export const Parrot = factory(function Parrot({ middleware: { icache } }) {
  // get the current name value or an empty string
  const name = icache.getOrSet("name", "");
  return (
    <virtual>
      <h3 classes={[css.root]}>{`Polly: ${name}`}</h3>
      <input
        classes={[css.input]}
        placeholder="Polly want a cracker?"
        type="text"
        onkeyup={event => {
          // when the cache is updated, it will
          // handle calling the invalidator
          icache.set(
            "name",
            (event.target as HTMLInputElement).value
          );
        }}
      />
    </virtual>
  );
});

export default Parrot;

This would be equivalent to the @watch decorator that you can use with class based widgets. I would guess that 99% of the time, you would use icache to manage local state in your widgets.

Application Store

There are a number of ways you could work with stores in Dojo. You could use containers or a provider. Or, you could use a store middleware!

We can create a store middleware that will hold a list of users.

// src/middleware/store.ts
import createStoreMiddleware from "@dojo/framework/core/middleware/store";
import { User } from "../interfaces";

export default createStoreMiddleware<{ users: User[] }>();

Now, we need a way to retrieve a list of users. We could do that via a process, which is how you can manage application behavior.

We can build a process that will fetch some user data.

// src/processes/userProcess.ts
import {
  createCommandFactory,
  createProcess
} from "@dojo/framework/stores/process";
import { replace } from "@dojo/framework/stores/state/operations";

const commandFactory = createCommandFactory();

const fetchUsersCommand = commandFactory(async ({ path }) => {
  const response = await fetch("https://reqres.in/api/users");
  const json = await response.json();
  return [replace(path("users"), json.data)];
});

export const getUsersProcess = createProcess("fetch-users", [
  fetchUsersCommand
]);

With a store and a process ready to go, we can use them in a widget that will display our list of users.

// src/widgets/Users/Users.tsx
import { create, tsx } from "@dojo/framework/core/vdom";

import * as css from "./Users.m.css";

import store from "../../middleware/store";
import { fetchUsersProcess } from "../../processes/userProcesses";
import { User } from "../../interfaces";

// pass store to render factory
// as middleware
const render = create({ store });

// helper method to render list of Users
const userList = (users: User[]) =>
  users.map(user => (
    <li key={user.id} classes={[css.item]}>
      <img
        classes={[css.image]}
        alt={`${user.first_name} ${user.last_name}`}
        src={user.avatar}
      />
      <span classes={[css.title]}>
        {user.last_name}, {user.first_name}
      </span>
    </li>
  ));

export default render(function Users({ middleware: { store } }) {
  // extract helper methods from the store in widget
  const { get, path, executor } = store;
  // get current value of Users
  const users = get(path("users"));
  if (!users) {
    // if no Users, run the `executor` against
    // the process to fetch a list of Users
    executor(fetchUsersProcess)(null);
    // since the process to fetch Users does not need
    // any arguments, execute with null

    // if the network is slow, return
    // a loading message
    return <em>Loading users...</em>;
  }

  return (
    <div classes={[css.root]}>
      <h1>Users</h1>
      <ul classes={[css.list]}>{userList(users)}</ul>
    </div>
  );
});

The key here is that the store middleware has an executor method that can be used to execute processes directly from your widget.

executor(fetchUsersProcess)(null);

In this case, the fetchUsersProcess does not expect a payload, so we can pass null to it. If it needed to do pagination for example, we could pass which page we wanted as an argument and use it in our process.

You can see this demo in action here.

Summary

There's more middleware available that we didn't cover in this post, related to theming, i18n, DOM related, and interacting with the render method. We'll cover most of these in future blog posts!

I'm really excited about all the new features in this latest release of Dojo and working with the available middleware and even what I could do with a custom middleware!