Mithril’s m.request: The One‑Stop HTTP Helper for SPAs
Learn how Mithril’s m.request automatically parses JSON, caches responses, supports cancellation, and triggers redraws. A concrete example shows how to fetch data, cancel in‑flight requests, and keep your UI in sync.
06 Apr 2026, 09:55 UTC

Problem: Manual HTTP in SPAs is a pain
When building a single‑page application (SPA) you often need to fetch data, parse it, cache it, and keep the UI in sync. Vanilla fetch requires you to write repetitive boilerplate for JSON parsing, error handling, and UI updates. In a Mithril project, you’d normally invoke fetch inside a component, then call m.redraw() manually after the promise resolves.
Thesis: m.request is a ready‑made HTTP helper
In Mithril, m.request bundles the common patterns into a single API:
- Automatic JSON deserialization
- Lightweight request‑based caching
- Abortable requests via
abort() - Automatic redraw integration when data arrives
These features reduce boilerplate, improve performance, and make your components cleaner.
1. Automatic JSON Handling
When you call m.request with method and url, Mithril uses the native fetch API under the hood. If the response’s Content‑Type header contains application/json, the library automatically runs response.json() and returns a plain JavaScript object. You can then use the data directly without JSON.parse().
Example:
m.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/todos/1'
}).then(data => {
console.log(data.title); // "delectus aut autem"
});
2. Lightweight Caching Layer
m.request caches responses keyed by the combination of request url, method, and any params or data you pass. Subsequent identical requests return the cached promise, preventing duplicate network traffic. The cache is in-memory and has no expiration policy; it lives as long as the page session.
To verify caching, add a timestamp to the request options and log it:
const options = { method: 'GET', url: 'https://jsonplaceholder.typicode.com/todos/1', timestamp: Date.now() };
m.request(options).then(data => console.log('First call', options.timestamp));
m.request(options).then(data => console.log('Second call', options.timestamp));
Both logs will show the same timestamp, indicating the second call hit the cache.
3. Request Cancellation
m.request returns a promise that exposes an abort() method. This is handy when a component unmounts before the request finishes. Calling abort() cancels the underlying fetch request and prevents the promise from resolving.
Example in a component:
class Todo {
oninit(vnode) {
this.promise = m.request({
method: 'GET',
url: vnode.attrs.id
});
this.promise.then(data => this.todo = data);
}
onunload() {
this.promise.abort();
}
view() {
return this.todo
? m('div', this.todo.title)
: m('div', 'Loading...');
}
}
After abort() the promise never resolves, so the UI stays unchanged.
4. Automatic Redraw Integration
When m.request resolves, Mithril automatically triggers a redraw, ensuring that any view that depends on the fetched data updates without calling m.redraw() manually. This keeps the UI declarative and eliminates race conditions between async data and rendering.
Worked Example: A Todo Viewer
- Define a component that fetches a todo item and displays its title.
- Use
m.requestwithabort()inonunloadto cancel if the component is removed. - Show loading state while waiting.
- Verify that the component updates automatically after the request resolves.
Code:
const TodoViewer = {
oninit(vnode) {
this.loading = true;
this.promise = m.request({
method: 'GET',
url: `https://jsonplaceholder.typicode.com/todos/${vnode.attrs.id}`
}).then(data => {
this.todo = data;
this.loading = false;
});
},
onunload() {
this.promise.abort();
},
view() {
if (this.loading) return m('div', 'Loading...');
return m('div', [
m('h2', this.todo.title),
m('p', `Completed: ${this.todo.completed}`)
]);
}
};
Mount the component:
m.mount(document.body, { view: () => m(TodoViewer, { id: 1 }) });
When the request resolves, Mithril redraws the component automatically, rendering the title and completion status.
Trade‑offs and Limitations
- JSON only: m.request automatically parses JSON but will not handle XML, plain text, or binary blobs. For those formats, you must fall back to
fetchor a custom wrapper. - Simple cache: The cache has no expiration or invalidation policy. In applications where data changes frequently, you may need to disable caching or implement a manual cache busting strategy.
- No retry logic: m.request does not retry failed requests by default. If you need exponential backoff or retry limits, you’ll need to add that logic yourself.
- Abort side effects: Canceling a request may leave your component in a partially loaded state if you rely on multiple concurrent requests.
Actionable Take‑aways
- Use
m.requestfor most JSON‑based API calls to reduce boilerplate. - Leverage the built‑in cache for idempotent GET requests, but remember it never expires.
- Always abort in‑flight requests in
onunloadto prevent memory leaks and unwanted UI updates. - When testing components, mock
m.requestby providing a custom implementation that returns a resolved promise. - For non‑JSON data or advanced caching, combine
m.requestwith the nativefetchAPI or a third‑party HTTP client.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.