ripple-file-router
v0.0.19
Published
A minimal, lightweight SPA router system designed for RippleJS. Automatically generates routes from your .ripple pages, supports nested directories, includes global router loader and persistent state manager.
Maintainers
Readme
ripple-file-router
A minimal, file-based routing system for Ripple.js. Automatically generates routes from your .ripple pages, supports nested directories, includes global router loader and persistent state manager. Structure your pages/ directory and start routing with a single component.
Installation
npm install ripple-file-router
npx ripple-file-router init or
yarn add ripple-file-router
npx ripple-file-router init Quick Start
After initiating ripple-file-router, the pages directory, the routes.ts file for you app modules and the configured App.ripple file will be visible in your project src dir. The App.ripple is optional to overwrite.
Directory Structure
Here's an example pages/ directory:
pages/
├── index.ripple # Home page
├── about.ripple # About page
└── users/
└── [id]/
└── user/
└── [username]/
└── comments/Dynamic segments use [param] notation like [id] or [username].
App Component
import {PageRoutes} from "ripple-file-router"
import { modules } from "./routes";
export component App() {
<PageRoutes modules={modules} />
}That's it! Your routing is now set up. PageRoutes automatically reads your pages/ folder and matches routes, including dynamic parameters.
Link Component
Use the Link component for navigation:
import Link from "ripple-file-router"
export component Navigation() {
<nav>
<Link href="/"><p>{"Home"}</p></Link>
<Link emitEvent={false} href="/about"><p>{"About"}</p></Link>
<Link href="/users/42/user/john" queries={{name: "John", age: 20}}><p>{"User Profile"}</p></Link>
</nav>
}Props:
| Prop | Type | Default | Description |
| ------------------ | --------------------- | ------- | --------------------------------------------- |
| href | string | — | Path to navigate to |
| children | Component | — | Content to render inside link |
| onLoading | () => void | — | Callback when navigation starts |
| emitEvent | boolean | true | Whether to trigger route events for this link |
| loadingComponent | Component | — | Optional component to show while loading |
| className | string | — | Additional CSS class names for styling |
| queries | Record<string, any> | — | Optional query parameters for URLSearch |
Router And Events
You can subscribe to router events if you need custom behavior:
import { useRouter } from "ripple-file-router"
const router = useRouter();
router.on("start", path => console.log("Navigating to:", path));
router.on("complete", path => console.log("Navigation finished:", path));
router.on("change", path => console.log("Route changed:", path));
//Guard back navigation
router.beforePopState((url) => {
if (url === "/protected") {
console.log("Navigation blocked:", url);
return false; // Cancel navigation
}
});
// Navigate to a new route
router.push("/users/42?tab=posts");
router.push("/users/42?tab=posts", true, false, {name: "John", age: 20}); // path, emitEvent, shallow (partial url change), queries
//Replace URL shallowly (no full sync)//
router.replace("/users/42?tab=profile", true, true);
// Prefetch a route
router.prefetch("/about");
// Resolve href
console.log("Resolved href:", router.resolveHref("/contact?ref=home"));
// Access reactive properties
console.log("Current path:", router.path);
console.log("Query params:", router.queries);
console.log("Dynamic params:", router.params);
console.log("Full URL:", router.asPath);
// Access full URL info
console.log(router.host);
console.log(router.hostname);
console.log(router.origin);
console.log(router.protocol);
console.log(router.port);
console.log(router.href);
console.log(router.search);
console.log(router.hash);start: triggered before navigation beginscomplete: triggered after navigation finisheschange: triggered on every path change
You can opt out of events per Link with emitEvent={false}.
Dynamic Route Params
Access route params and queries in any component:
import { useRouter } from "ripple-file-router"
export component UserProfile() {
const router = useRouter();
const id = router.params.id; // dynamic param
const username = router.params.username;
const queryName = router.queries.name; // URL query ?name=...
// or
const {params, queries} = router;
<div>
{"User ID: " + id}
{"Username: " + username}
{"Query name: " + queryName}
</div>
}Global Loading Indicator (Optional)
you can disable it with props ```ts
```ts
import {PageRoutes} from "ripple-file-router"
import { modules } from "./routes";
export component App() {
<PageRoutes modules={modules} enableLoader={false} />
}
A minimal reactive store that manages shared state across your app with an intuitive API. It provides reactivity, persistence, and derived state — all in under a few lines of code.
| Feature | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------- |
| get() | Returns the current store value. |
| set(next) | Replaces the entire store value. |
| update(partialOrFn) | Merges new data into the store. Supports both object patching and callback styles. |
| subscribe(fn, selector?) | Reactively listens for state changes, optionally to a selected portion. |
| derive(selector) | Creates a new store derived from a specific part of another store (like computed state). |
| delete(keys) | Removes one or more keys from the store. |
| clear() | Resets store to initial state and removes persisted data. |
| persist (option) | Automatically saves and restores state from localStorage. |
-------------------- Example Stores --------------------
//* Route store for storing current route path
//* Persisted in localStorage as "routeStore"
//*/
export const routeStore = createStore(
{ path: "/" },
{ persist: true, storageKey: "routeStore" }
);
/**
* App store for global state
* Tracks path, user info, theme
* Persisted in localStorage as "appStore"
*/
export const appStore = createStore(
{ path: "/", user: null as null | { name: string }, theme: "light" },
{ persist: true, storageKey: "appStore" }
);
Here are extra two simple Hello World store examples for getting started and explain things better.
Store without persist (default)
import { createStore } from "ripple-file-router"
// Create a simple store
const helloStore = createStore({ message: "Hello World!" });
// Subscribe to changes
helloStore.subscribe(state => {
console.log("Current message:", state.message);
});
// Get changes anywhere
const data = helloStore.get();
console.log(helloStore) // { message: Current message}
console.log(data.message) // Current message
// Update the store
helloStore.update({ message: "Hello Ripple!" });
// Output:
// Current message: Hello World!
// Current message: Hello Ripple!
Store with persist
import { createStore } from "ripple-file-router"
import { track } from "ripple"
const message = track("")
// Create a persisted store
const persistentHelloStore = createStore(
{ message: "Hello Persistent World!" },
{ persist: true, storageKey: "helloStore" }
);
// Subscribe to changes
persistentHelloStore.subscribe(state => {
console.log("Current message:", state.message);
});
// Get changes anywhere
const data = helloStore.get();
console.log(helloStore) // { message: Current message}
console.log(data.message) // Current message
// Update the store
persistentHelloStore.update({ message: "Updated and Persisted!" });
// Callback update (safe addition)
persistentHelloStore.update(prev => ({ message: prev.message + " " + @message }));
// Reload the page and subscribe again
persistentHelloStore.subscribe(state => {
console.log("After reload:", state.message);
});
// Output (after reload):
// After reload: Updated and Persisted!
export const appStore = createStore(
{
user: { name: "Joe", location: "unknown", preferences: [] },
count: 0,
theme: "light",
},
{ persist: true, storageKey: "appStore" }
);
// Subscribe to entire state
appStore.subscribe(s => console.log("State changed:", s));
// Watch a specific value
appStore.watch(s => s.count, (n, o) => console.log(`Count: ${o} → ${n}`));
// Use middleware for logging
appStore.use((state, action, payload) =>
console.log(`[${action}]`, payload, state)
);
// Partial update
appStore.update({ count: 1 });
// Callback update (safe addition)
appStore.update(prev => ({ count: prev.count + 1 }));
// Derived store
const themeStore = appStore.derive(s => s.theme);
themeStore.subscribe(theme => console.log("Theme:", theme));
// Clear store
appStore.clear();
Here’s a concise side-by-side comparison between ripple-file-router createStore and Zustand:
| Feature / Aspect | createStore (ripple-file-router) | Zustand |
| ------------------------ | ------------------------------------ | ---------------------------------------- |
| Size / Complexity | Ultra-light (~2 KB) | Larger, includes middleware and devtools |
| Reactivity Model | Manual subscribe / derive | React hooks (useStore) |
| Selectors | Optional selector argument | Built-in via hooks |
| Persistence | Native persist option | Needs middleware plugin |
| DevTools Integration | Coming soon | Built-in Redux DevTools support |
| Middleware | Planned via use() | Full middleware API |
| Callback Updates | Supported: update(prev => {...}) | Supported: set(state => {...}) |
| Derived Stores | derive(selector) | Selectors or derived state |
| Performance | Minimal overhead | Optimized for React, slightly heavier |
| Framework Support | Framework-agnostic | React-only |
| TypeScript | Fully typed generics | Excellent TS support |
| Persistence Control | Built-in localStorage | Plugin required |
| Use Case Fit | Libraries & multi-framework projects | React apps needing global state |
Features
- File-based routing
- Dynamic route segments
[param] - URL query support
- Optional per-link router events
- Reactive
Linkcomponent with optional loading UI - Global progress loader
- Minimal setup—just structure
pages/ - Zustand like global state management available in and outside components
