npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ejsc-ma-router

v1.0.10

Published

High-performance React Router tailored for 365 Mini App ecosystem. Features Framer Motion page transitions, declarative navigation config, and strict mobile stack-based navigation paradigms.

Readme

@ejsc/ma-router 🚀

npm version License: MIT

The High-Performance Stack Router for 365 Mini App Applications.

ejsc-ma-router là giải pháp điều hướng (Routing) chuyên biệt cho hệ sinh thái Mini App. Không giống như các router web thông thường, nó được tối ưu hóa cho mô hình Stack-based Navigation, hỗ trợ Keep-alive (giữ trạng thái trang), Page Transitions mượt mà và các vòng đời (Lifecycle) đặc thù của Mini App như useDidShow, useDidHide.



Cài đặt

pnpm add ejsc-ma-router
# hoặc
npm install ejsc-ma-router

Import CSS animation (trong entry file):

import 'ejsc-ma-router/styles.css';

Bắt đầu nhanh

// src/app.config.ts
import { lazy } from 'react';
import type { IRouterConfig } from 'ejsc-ma-router';

const config: IRouterConfig = {
  pages: [
    {
      pathname: '/pages/home/index',
      Component: lazy(() => import('./pages/home')),
    },
    {
      pathname: '/pages/profile/index',
      Component: lazy(() => import('./pages/profile')),
    },
  ],
  animation: {
    type: 'slide_left',
    mode: 'style-transition',
  },
  keepAlive: {
    enable: true,
    maxStack: 5,
    freeze: true,
    freezeDelay: 300,
  },
};

export default config;
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Router } from 'ejsc-ma-router';
import 'ejsc-ma-router/styles.css';
import config from './app.config';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <Router config={config} />
);

API

<Router />

Component gốc — khởi tạo router và render page stack.

import { Router } from 'ejsc-ma-router';

<Router config={config} />
// hoặc truyền function (lazy config)
<Router config={() => buildConfig()} />

| Prop | Type | Mô tả | |---|---|---| | config | IRouterConfig \| () => IRouterConfig | Cấu hình router |


<Link />

Điều hướng khai báo, thay thế thẻ <a>.

import { Link } from 'ejsc-ma-router';

// Điều hướng tới pathname
<Link to="/pages/profile/index">Profile</Link>

// Với params và state
<Link to="/pages/detail/index" params={{ id: '123' }} state={{ from: 'home' }}>
  Chi tiết
</Link>

// Replace (không thêm vào history stack)
<Link to="/pages/login/index" replace>Đăng nhập</Link>

// Quay lại (delta âm)
<Link to={-1}>Quay lại</Link>
<Link to={-2}>Quay lại 2 bước</Link>

useNavigate()

Hook điều hướng programmatic.

import { useNavigate } from 'ejsc-ma-router';

const navigate = useNavigate();

// Push
navigate('/pages/detail/index');

// Push với params
navigate('/pages/detail/index', { params: { id: '42' } });

// Push với state
navigate('/pages/detail/index', { state: { from: 'list' } });

// Replace
navigate('/pages/home/index', { replace: true });

// Quay lại
navigate(-1);

// Quay lại 2 bước
navigate(-2);

// Với animation override
navigate('/pages/modal/index', {
  animation: { type: 'slide_up' },
  keepAlive: false,
});

useLocation()

Trả về location của page hiện tại.

import { useLocation } from 'ejsc-ma-router';

const location = useLocation();
// { key: 'abc123', pathname: '/pages/detail/index', params: { id: '42' }, state: {...} }

console.log(location.pathname); // '/pages/detail/index'
console.log(location.params?.id); // '42'
console.log(location.state);

useHistory() / useHistories()

import { useHistory, useHistories } from 'ejsc-ma-router';

// History hiện tại
const history = useHistory();
// { action: 'PUSH', location: { key, pathname, params, state } }

// Toàn bộ history stack
const histories = useHistories();
console.log(histories.length); // số page đang trong stack

useNavigationType()

Trả về action của navigation gần nhất: 'PUSH' | 'REPLACE' | 'POP'.

import { useNavigationType } from 'ejsc-ma-router';

const action = useNavigationType();

if (action === 'POP') {
  // user đang quay lại
}

useAppState()

Đọc và cập nhật app-level state (animation, keepAlive config).

import { useAppState } from 'ejsc-ma-router';

const { config, appState, setAppState } = useAppState();

// Tắt animation tạm thời
setAppState((prev) => ({
  ...prev,
  animation: { type: 'none' },
}));

useDidShow()

Fires khi page trở thành active (visible). Tương đương onShow trong mini app native.

import { useDidShow } from 'ejsc-ma-router';

// Chỉ lấy trạng thái
const isVisible = useDidShow();

// Với callback
useDidShow(() => {
  console.log('Page hiện ra — refresh data');
  fetchData();
});

useDidHide()

Fires khi page bị ẩn (không còn active). Tương đương onHide.

import { useDidHide } from 'ejsc-ma-router';

useDidHide(() => {
  console.log('Page bị ẩn — dừng timer');
  clearInterval(timer);
});

useAppPause()

Fires khi app vào background (visibilitychange → hidden).

import { useAppPause } from 'ejsc-ma-router';

useAppPause(() => {
  console.log('App vào background — lưu state');
  saveState();
});

useAppResume()

Fires khi app quay lại foreground (visibilitychange → visible).

import { useAppResume } from 'ejsc-ma-router';

useAppResume(() => {
  console.log('App quay lại — sync data');
  syncData();
});

useSwipeNavigation() / onSwipeNavigation()

Detect swipe-back gesture.

import { useSwipeNavigation, onSwipeNavigation } from 'ejsc-ma-router';

// Hook — ref.current = true khi đang swipe
const isSwipingRef = useSwipeNavigation();

// Subscribe bên ngoài component
const unsubscribe = onSwipeNavigation((isSwiping) => {
  console.log('Swipe navigation:', isSwiping);
});
// cleanup
unsubscribe();

Cấu hình (IRouterConfig)

type IRouterConfig = {
  pages: IRouterPageConfig[];          // Danh sách pages (bắt buộc)
  animation?: IAnimationConfig;        // Animation mặc định cho toàn app
  keepAlive?: IKeepAliveState;         // Keep-alive mặc định cho toàn app
  Layouts?: ILayout[];                 // Layout wrappers mặc định
  NotFoundPage?: ComponentType;        // Trang 404
  ErrorPage?: ComponentType;           // Trang lỗi (ErrorBoundary fallback)
  navigation?: {
    alwaysRootPage?: boolean;          // Luôn giữ root page trong stack
  };
};

IRouterPageConfig

type IRouterPageConfig = {
  pathname: string;                    // Đường dẫn (bắt buộc), vd: '/pages/home/index'
  Component: ComponentType;            // React component (bắt buộc)
  title?: string;                      // Tiêu đề page
  Layouts?: ILayout[];                 // Layout override cho page này
  bottomTabBarId?: string;             // ID tab bar (nếu dùng tab navigation)
  animation?: IAnimationConfig;        // Animation override cho page này
  keepAlive?: boolean;                 // Giữ page trong DOM khi không active
  freeze?: boolean;                    // Freeze rendering khi không active
  freezeDelay?: number;                // Delay (ms) trước khi freeze
};

IAnimationConfig

type IAnimationConfig = {
  type?: 'none' | 'slide_up' | 'slide_left' | 'fade_in';
  mode?: 'view-transition' | 'style-transition';
};

| type | Mô tả | |---|---| | none | Không có animation | | slide_left | Slide từ phải sang trái (mặc định cho push) | | slide_up | Slide từ dưới lên (dùng cho modal/bottom sheet) | | fade_in | Fade in/out |

| mode | Mô tả | |---|---| | style-transition | CSS class-based (mặc định, tương thích rộng) | | view-transition | Web View Transitions API |

IKeepAliveState

type IKeepAliveState = {
  enable?: boolean;     // Bật keep-alive toàn app
  maxStack?: number;    // Số page tối đa giữ trong stack (mặc định: 5)
  freeze?: boolean;     // Freeze DOM khi page không active
  freezeDelay?: number; // Delay (ms) trước khi freeze (mặc định: 0)
};

Router Events

Dùng routerEvents để lắng nghe các sự kiện navigation từ bên ngoài component.

import { routerEvents } from 'ejsc-ma-router';

// Trước khi navigation xảy ra
const off = routerEvents.on('beforeNavigate', ({ to, history, nextHistory }) => {
  console.log('Sắp navigate tới:', to);
});

// Sau khi navigation hoàn tất
routerEvents.on('afterNavigate', ({ to, history, prevHistory }) => {
  console.log('Đã navigate tới:', to);
  analytics.track('page_view', { pathname: history.location.pathname });
});

// Sau khi router khởi tạo xong
routerEvents.on('afterInit', ({ history, histories }) => {
  console.log('Router đã init, page đầu tiên:', history.location.pathname);
});

// Cleanup
off();

Danh sách events:

| Event | Payload | Mô tả | |---|---|---| | beforeNavigate | IBeforeNavigateData | Trước khi navigate | | afterNavigate | IAfterNavigateData | Sau khi navigate xong | | beforeHistoryChange | IBeforeHistoryChangeData | Trước khi history stack thay đổi | | afterHistoryChange | IAfterHistoryChangeData | Sau khi history stack thay đổi | | beforeHistoryChangeP2 | IBeforeHistoryChangeData | Phase 2 (dùng cho animation) | | afterHistoryChangeP2 | IAfterHistoryChangeData | Phase 2 (dùng cho animation) | | afterInit | IAfterInitData | Sau khi router init xong |


Layouts

Layout là component bọc ngoài page, dùng để inject header, footer, tab bar, v.v.

// Định nghĩa layout
const AppLayout: ILayout = ({ children }) => (
  <div className="app-shell">
    <Header />
    <main>{children}</main>
    <TabBar />
  </div>
);

// Áp dụng cho toàn app
const config: IRouterConfig = {
  pages: [...],
  Layouts: [AppLayout],
};

// Áp dụng cho từng page (override)
{
  pathname: '/pages/modal/index',
  Component: ModalPage,
  Layouts: [], // không có layout (full screen)
}

Nhiều layouts được nest từ ngoài vào trong:

Layouts: [OuterLayout, InnerLayout]
// → <OuterLayout><InnerLayout><Page /></InnerLayout></OuterLayout>

Selectors (Zustand)

Dùng trực tiếp với useRouterStore khi cần truy cập store nâng cao:

import { useRouterStore } from 'ejsc-ma-router/stores/router';
import {
  historiesSelector,
  lastHistorySelector,
  lastLocationSelector,
  pageConfigSelector,
  lastPageConfigSelector,
  pageStateSelector,
} from 'ejsc-ma-router/stores/router.selector';

// Lấy toàn bộ history stack
const histories = useRouterStore(historiesSelector);

// Lấy config của page hiện tại
const pageConfig = useRouterStore(lastPageConfigSelector);

// Lấy config của page theo locationKey
const config = useRouterStore(pageConfigSelector(locationKey));

Utilities

import { PAGE_PATH_NAME } from 'ejsc-ma-router';
// PAGE_PATH_NAME = '__pagePath' — query param key dùng nội bộ

import { objectToSearchString } from 'ejsc-ma-router/utils/url';
objectToSearchString({ id: '1', tab: 'info' }); // '?id=1&tab=info'

import { setupAnimation, ANIMATION_DURATION } from 'ejsc-ma-router/utils/animation';
// ANIMATION_DURATION = 250 (ms)

import { analytic, AnalyticEventName } from 'ejsc-ma-router/utils/analytic';
analytic.sendEvent({ event: AnalyticEventName.PAGE_VIEW, params: { pathname: '/home' } });

import { deepCloneWithoutComponent } from 'ejsc-ma-router/utils/deep-clone';
import { EventEmitter } from 'ejsc-ma-router/utils/event-emitter';
import { classNames } from 'ejsc-ma-router/utils/classname';

Components nội bộ

Các component này được dùng bởi <Router /> nhưng có thể import trực tiếp nếu cần:

import { ErrorBoundary } from 'ejsc-ma-router/components/error';
import { Freeze } from 'ejsc-ma-router/components/freeze';
import { PageLoading } from 'ejsc-ma-router/components/layout';
import { LayoutsRender } from 'ejsc-ma-router/components/router/render-layout';
import { PagesRender } from 'ejsc-ma-router/components/router/pages-render';
import { StylePagesRender } from 'ejsc-ma-router/components/router/style-pages-render';

<ErrorBoundary />

<ErrorBoundary Component={CustomErrorPage}>
  <MyPage />
</ErrorBoundary>

<Freeze />

Keep-alive component — giữ DOM nhưng suspend rendering.

<Freeze freeze={!isActive} delay={300} placeholder={<PageLoading />}>
  <ExpensivePage />
</Freeze>

| Prop | Type | Mô tả | |---|---|---| | freeze | boolean | Bật/tắt freeze | | delay | number | Delay (ms) trước khi freeze | | placeholder | ReactNode | Hiển thị khi đang frozen |


Cấu trúc thư mục

src/
├── components/
│   ├── error/
│   │   ├── error-boundary.tsx   # ErrorBoundary class component
│   │   └── index.ts
│   ├── freeze/
│   │   └── index.tsx            # Freeze keep-alive component
│   ├── layout/
│   │   ├── page-loading.tsx     # Loading spinner
│   │   └── index.ts
│   ├── link/
│   │   └── index.tsx            # <Link /> component
│   └── router/
│       ├── router.tsx           # <Router /> — entry component
│       ├── pages-render.tsx     # Render stack với keep-alive
│       ├── style-pages-render.tsx # Render với CSS transitions
│       ├── render-layout.tsx    # <LayoutsRender /> helper
│       └── index.ts
├── context/
│   └── location-key.ts          # LocationKeyContext — inject locationKey vào page
├── hooks/
│   ├── use-app-pause.ts
│   ├── use-app-resume.ts
│   ├── use-app-state.ts
│   ├── use-did-hide.ts
│   ├── use-did-show.ts
│   ├── use-history.ts           # useHistory, useHistories
│   ├── use-location.ts
│   ├── use-navigate.ts
│   ├── use-navigation-type.ts
│   ├── use-swipe-navigation.ts
│   └── index.ts
├── stores/
│   ├── router.ts                # Zustand store + routerEvents EventEmitter
│   └── router.selector.ts      # 10 selectors
├── types/
│   ├── navigation.ts            # INavigate, ILocation, IHistory, IHistoryAction...
│   ├── router.ts                # IRouterConfig, IRouterPageConfig, IAnimationConfig...
│   └── index.ts
├── utils/
│   ├── analytic.ts              # analytic.sendEvent()
│   ├── animation.ts             # setupAnimation(), ANIMATION_DURATION
│   ├── classname.ts             # classNames (re-export classnames)
│   ├── deep-clone.ts            # deepCloneWithoutComponent()
│   ├── event-emitter.ts         # EventEmitter<T> class
│   ├── history.ts               # initHistories, getPageByPathname, PAGE_PATH_NAME
│   └── url.ts                   # objectToSearchString()
├── index.ts                     # Public API
└── styles.css                   # Animation + page container CSS

Build

cd src/miniapp/ejsc-ma-router

pnpm install
pnpm build       # tsc → dist/
pnpm dev         # tsc --watch
pnpm typecheck   # kiểm tra types không emit

Dependencies

| Package | Mô tả | |---|---| | zustand | State management cho router store | | nanoid | Tạo unique locationKey cho mỗi navigation entry | | classnames | Utility ghép CSS class names | | lodash | Utility functions | | react-freeze | Suspend rendering cho keep-alive pages |


Types

import type {
  // Config
  IRouterConfig,
  IRouterPageConfig,
  IAnimationConfig,
  IAnimationState,
  INavigationAnimationType,
  INavigationAnimationMode,
  IKeepAliveState,
  ILayout,
  IRouterAppState,
  IRouterPageState,

  // Navigation
  INavigate,
  INavigatePathnameOptions,
  INavigateDeltaOptions,
  ILocation,
  IHistory,
  IHistoryAction,
  IHistoryState,
} from 'ejsc-ma-router';

� 2026 EJSC Technology. All rights reserved.


© 2026 EJSC Technology. All rights reserved.