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
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!