Sponsored Content

DEV Community

Pijar Sukma Adiluhung
Pijar Sukma Adiluhung

Posted on

Indonesians! Your Hijri Calendar Widget Shouldn't Need a Network Call. So I Fixed That.

A while back I wrote about building mabims.dev, a free open-source API for Indonesia's Hijri calendar based on MABIMS criteria (not Umm al-Qura, which is what most Hijri libraries default to and which can be off by a day from what's officially observed here).

But an API is still an API... every time your widget renders, you're firing a network request just to answer "what's today's Hijri date?" For something that changes once a day and barely ever needs live computation, that felt like overkill.

So I built mabims-hijri, an npm package that bundles the calendar data directly into your app. Most lookups resolve instantly, with zero network calls.

The Idea

Hijri dates for 2024–2026 are already known, they're not live-computed, they come from an official curated table. So instead of hitting an API for a fact that doesn't change, the package ships that table (~72KB of JSON) right inside itself.

npm install mabims-hijri
Enter fullscreen mode Exit fullscreen mode
import { today } from 'mabims-hijri'

const date = await today()
console.log(date.output)
// {
//   date: '1448-03-18',
//   calendar: 'hijri',
//   day: 18,
//   month: 3,
//   month_name: 'Rabiul Awal',
//   year: 1448
// }
Enter fullscreen mode Exit fullscreen mode

That call resolves from bundled data. No fetch, no waiting, works offline.

Three-Tier Fallback

For dates outside the bundled range (post-2026), it falls back gracefully instead of just failing:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Bundled MABIMS    β”‚ ──► β”‚  In-Memory Cache β”‚ ──► β”‚  Live API         β”‚
β”‚  (data.json)       β”‚     β”‚  (per-session)    β”‚     β”‚  (mabims.dev)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      Built-in                 Always fast            Fallback only
      2024–2026                (TTL: 24h)             (outside range)
Enter fullscreen mode Exit fullscreen mode
  1. Bundled data: instant, no network, covers 2024-01-13 to 2026-12-31.
  2. In-memory cache: anything fetched from the API gets cached for 24h so repeat lookups stay fast.
  3. Live API: only touched when you ask for a date outside the bundled range.

The package also silently checks once a day for a newer version of the table and updates itself in the background, so it stays current without you having to bump the package version by hand.

Converting Dates

Same offline-first behavior applies to conversion, not just "today":

import { convert } from 'mabims-hijri'

// Gregorian β†’ Hijri
const hijri = await convert('2026-08-31')
console.log(hijri.output.month_name) // 'Rabiul Awal'

// Hijri β†’ Gregorian
const gregorian = await convert('1448-03-18', 'hijri')
Enter fullscreen mode Exit fullscreen mode

Rendering a Widget (React example)

import { useState, useEffect } from 'react'
import { today, type HijriDate } from 'mabims-hijri'

function HijriToday() {
  const [date, setDate] = useState<HijriDate | null>(null)

  useEffect(() => {
    today().then((res) => setDate(res.output))
  }, [])

  if (!date) return <span>Loading...</span>

  return <span>{date.day} {date.month_name} {date.year} H</span>
}
Enter fullscreen mode Exit fullscreen mode

First render, no spinner needed in practice! The bundled lookup is fast enough that useEffect resolves almost immediately.

Works Anywhere fetch Exists

Node 18+, browsers, edge runtimes (Cloudflare Workers, Vercel Edge, Deno), and React Native. For React Native, there's no localStorage, so persistent caching needs a storage adapter... but good news! The package exposes a simple interface for that:

import { setStorageAdapter } from 'mabims-hijri'
import AsyncStorage from '@react-native-async-storage/async-storage'

setStorageAdapter({
  get: (key) => AsyncStorage.getItem('mabims_' + key),
  set: (key, value) => AsyncStorage.setItem('mabims_' + key, value),
  has: async (key) => (await AsyncStorage.getItem('mabims_' + key)) !== null,
})
Enter fullscreen mode Exit fullscreen mode

Without it, caching just falls back to in-memory (works fine, just doesn't persist across app restarts).

Other Things It Covers

Beyond today() and convert(), the package wraps the full API surface: range() for bulk date conversion, month()/year() for rendering calendar grids, events() for Islamic holidays (Ramadan, Idul Fitri, Idul Adha, etc.), and hilal.info() for moon-visibility data used to determine the start of a Hijri month. Full reference is in the README.

Try It

npm install mabims-hijri
Enter fullscreen mode Exit fullscreen mode

MIT licensed, zero dependencies beyond native fetch. If you build something with it or hit an edge case, issues and PRs are welcome.

Top comments (0)