Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/router-solid-draft.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@effector/router-solid': minor
---

Add SolidJS bindings package `@effector/router-solid` (draft). Mirrors the
`@effector/router-react` API — `RouterProvider`, `createRouteView`,
`createLazyRouteView`, `createRoutesView`, `Link`/`useLink`, `Outlet`,
`withLayout`, `useRouter`, `useIsOpened`, `useOpenedViews` — implemented with
Solid primitives. Supports only the current major of SolidJS (1.x).
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A type-safe, framework-agnostic router built on top of [Effector](https://effect
| [`@effector/router-paths`](packages/paths) | Path parsing and matching utilities. |
| [`@effector/router-react`](packages/react) | React bindings for the router. |
| [`@effector/router-react-native`](packages/react-native) | React Native bindings for the router. |
| [`@effector/router-solid`](packages/solid) | SolidJS bindings for the router (draft). |

## Contributing

Expand Down Expand Up @@ -56,7 +57,7 @@ Run from the repository root:
| `pnpm changeset` | Create a changeset describing your change. |

To work on a single package, use the workspace shortcuts, e.g. `pnpm :core build` or `pnpm :react test`
(shortcuts: `:core`, `:paths`, `:react`, `:react-native`, `:docs`).
(shortcuts: `:core`, `:paths`, `:react`, `:react-native`, `:solid`, `:docs`).

### Running tests

Expand Down Expand Up @@ -91,7 +92,8 @@ router/
│ ├── core/ # @effector/router — core router
│ ├── paths/ # @effector/router-paths — path utilities
│ ├── react/ # @effector/router-react — React bindings
│ └── react-native/ # @effector/router-react-native — React Native bindings
│ ├── react-native/ # @effector/router-react-native — React Native bindings
│ └── solid/ # @effector/router-solid — SolidJS bindings (draft)
├── docs/ # VitePress documentation site
├── .changeset/ # pending changesets + config
└── .github/
Expand Down
5 changes: 5 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export default defineConfig({
},
],
},
{
text: 'Solid β',
collapsed: false,
items: [{ text: 'Overview', link: '/solid/' }],
},
],

footer: {
Expand Down
152 changes: 152 additions & 0 deletions docs/solid/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Solid

SolidJS bindings for effector/router, providing components and reactive helpers for seamless integration.

::: warning Draft
`@effector/router-solid` is an early draft. Its API mirrors [`@effector/router-react`](/react/)
and may change before the first stable release. Only the current stable major of SolidJS (**1.x**)
is supported.
:::

## Overview

`@effector/router-solid` provides Solid-specific utilities to use effector/router in your Solid applications:

- **Route Views** - Connect routes to Solid components
- **Navigation Components** - `Link` and navigation helpers
- **Reactive helpers** - Access router state as Solid accessors
- **Layouts** - Wrap multiple routes with shared layouts

Unlike the React binding, reactive helpers (`useLink`, `useIsOpened`, `useOpenedViews`) return Solid
**accessors** (`() => value`) instead of plain values.

## Installation

```bash
npm install @effector/router-solid @effector/router effector effector-solid solid-js
```

## Quick Example

```tsx
import { createRoute, createRouter } from '@effector/router';
import {
RouterProvider,
createRouteView,
createRoutesView,
Link,
} from '@effector/router-solid';

// 1. Create routes
const homeRoute = createRoute({ path: '/home' });
const aboutRoute = createRoute({ path: '/about' });

// 2. Create router
const router = createRouter({ routes: [homeRoute, aboutRoute] });

// 3. Bind routes to components
const HomeScreen = createRouteView({
route: homeRoute,
view: () => (
<div>
<h1>Home</h1>
<Link to={aboutRoute}>Go to About</Link>
</div>
),
});

const AboutScreen = createRouteView({
route: aboutRoute,
view: () => (
<div>
<h1>About</h1>
<Link to={homeRoute}>Go to Home</Link>
</div>
),
});

// 4. Render the currently opened route
const RoutesView = createRoutesView({
routes: [HomeScreen, AboutScreen],
});

// 5. Use in app
function App() {
return (
<RouterProvider router={router}>
<RoutesView />
</RouterProvider>
);
}
```

## Using Route Parameters

Access route parameters with effector-solid's `useUnit`, which returns a Solid accessor:

```tsx
import { useUnit } from 'effector-solid';

const userRoute = createRoute({ path: '/user/:id' });

const UserScreen = createRouteView({
route: userRoute,
view: () => {
const params = useUnit(userRoute.$params);
return <div>User ID: {params().id}</div>;
},
});
```

## Lazy Loading

Load route components on demand:

```tsx
import { createLazyRouteView } from '@effector/router-solid';

const ProfileScreen = createLazyRouteView({
route: profileRoute,
view: () => import('./screens/ProfileScreen'),
fallback: () => <div>Loading...</div>,
});
```

## Layouts

Share layouts across multiple routes:

```tsx
import { withLayout } from '@effector/router-solid';

const MainLayout = (props) => (
<div>
<header>Header</header>
{props.children}
<footer>Footer</footer>
</div>
);

const RoutesView = createRoutesView({
routes: [
...withLayout(MainLayout, [HomeScreen, AboutScreen, ContactScreen]),
LoginScreen, // Without layout
],
});
```

## APIs

The Solid binding mirrors the React one:

- `RouterProvider` — provide the router to the tree.
- `createRouteView` / `createLazyRouteView` — bind a route to a component (with optional lazy loading).
- `createRoutesView` — render the currently opened route, with an `otherwise` fallback.
- `Link`, `useLink` — declarative and imperative navigation.
- `withLayout` — share a layout across routes.
- `Outlet`, `useRouter`, `useIsOpened`, `useOpenedViews` — composition helpers.

## Next Steps

- [Core Package](/core/create-router) - Learn about core concepts
- [React bindings](/react/) - The stable reference implementation this binding mirrors
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
":docs": "pnpm --filter @effector/router-docs",
":react": "pnpm --filter @effector/router-react",
":react-native": "pnpm --filter @effector/router-react-native",
":solid": "pnpm --filter @effector/router-solid",
":paths": "pnpm --filter @effector/router-paths",
":core": "pnpm --filter @effector/router"
},
Expand All @@ -36,10 +37,13 @@
"@vitejs/plugin-react": "^4.3.4",
"effector": "^23.4.4",
"effector-react": "^23.3.0",
"@solidjs/testing-library": "^0.8.10",
"effector-solid": "^0.23.0",
"eslint": "10.0.2",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.5",
"globals": "^17.4.0",
"solid-js": "^1.9.14",
"happy-dom": "^20.8.3",
"history": "5.3.0",
"jiti": "^2.6.1",
Expand All @@ -53,6 +57,7 @@
"vite": "7.3.2",
"vite-plugin-babel": "1.5.1",
"vite-plugin-dts": "4.5.4",
"vite-plugin-solid": "^2.11.12",
"vitepress": "1.6.4",
"vitest": "4.0.18",
"zod": "^4.3.6"
Expand Down
89 changes: 89 additions & 0 deletions packages/solid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ☄️ @effector/router-solid

[![npm](https://img.shields.io/npm/v/@effector/router-solid.svg)](https://www.npmjs.com/package/@effector/router-solid)

> ⚠️ **Draft.** API mirrors [`@effector/router-react`](https://www.npmjs.com/package/@effector/router-react) and may change before the first stable release.

SolidJS bindings for [`@effector/router`](https://www.npmjs.com/package/@effector/router). Connect
routes to components, render the active route, and navigate with a `<Link>` — all driven by
Effector state.

Only the current major of SolidJS (**1.x**) is supported.

## Install

```bash
npm install @effector/router-solid @effector/router effector effector-solid solid-js
```

## Quick start

```tsx
import { createRoute, createRouter } from '@effector/router';
import {
RouterProvider,
createRouteView,
createRoutesView,
Link,
} from '@effector/router-solid';

// 1. Routes + router (see @effector/router for history setup)
const home = createRoute({ path: '/' });
const about = createRoute({ path: '/about' });
const router = createRouter({ routes: [home, about] });

// 2. Bind each route to a component
const HomeScreen = createRouteView({
route: home,
view: () => <Link to={about}>Go to About</Link>,
});
const AboutScreen = createRouteView({
route: about,
view: () => <Link to={home}>Back Home</Link>,
});

// 3. Render the active route
const Routes = createRoutesView({ routes: [HomeScreen, AboutScreen] });

export function App() {
return (
<RouterProvider router={router}>
<Routes />
</RouterProvider>
);
}
```

Read route params in a view with effector-solid's `useUnit` (returns a Solid accessor):

```tsx
import { useUnit } from 'effector-solid';

const UserScreen = createRouteView({
route: userRoute, // createRoute({ path: '/user/:id' })
view: () => {
const params = useUnit(userRoute.$params);
return <div>User {params().id}</div>;
},
});
```

## API

- `RouterProvider` — provide the router to the tree.
- `createRouteView` / `createLazyRouteView` — bind a route to a component (with optional lazy loading).
- `createRoutesView` — render the currently opened route, with an `otherwise` fallback.
- `Link`, `useLink` — declarative and imperative navigation.
- `withLayout` — share a layout across routes.
- `Outlet`, `useRouter`, `useIsOpened`, `useOpenedViews` — composition helpers.

Unlike the React binding, reactive helpers (`useLink`, `useIsOpened`, `useOpenedViews`) return Solid
**accessors** (`() => value`) instead of plain values.

## Documentation

Full guides and API reference: **[router.effector.dev/solid](https://router.effector.dev/solid)**

## License

[MIT](https://github.com/effector/router/blob/main/LICENSE)
8 changes: 8 additions & 0 deletions packages/solid/lib/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Router } from '@effector/router';
import { createContext } from 'solid-js';
import type { RouteView } from './types';

export const RouterProviderContext = createContext<Router | null>(null);
export const OutletContext = createContext<{ children: RouteView[] }>({
children: [],
});
52 changes: 52 additions & 0 deletions packages/solid/lib/create-lazy-route-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { lazy, Suspense } from 'solid-js';
import { is } from '@effector/router';
import type { InternalRoute } from '@effector/router';
import type { CreateLazyRouteViewProps, RouteView } from './types';

/**
* @description Creates Lazy route view with async bundle load
* @link https://router.effector.dev/solid/create-lazy-route-view.html
* @param props Lazy route view props
* @returns RouteView
* @example ```ts
* // profile.tsx
* export default function () {
* return <>...</>;
* }
*
* // index.ts
* import { createLazyRouteView } from '@effector/router-solid';
* import { routes } from '@shared/routing';
* import { MainLayout } from '@layouts';
*
* export const ProfileScreen = createLazyRouteView({
* route: routes.profile,
* view: () => import('./profile'),
* fallback: () => ':(',
* layout: MainLayout,
* });
* ```
*/
export function createLazyRouteView<T extends object | void = void>(
props: CreateLazyRouteViewProps<T>,
): RouteView {
if (!is.router(props.route)) {
(props.route as InternalRoute<T>).internal.setAsyncImport(props.view);
}

const View = lazy(props.view);
const { layout: Layout, fallback: Fallback } = props;

const inner = () => (
<Suspense fallback={Fallback ? <Fallback /> : null}>
<View />
</Suspense>
);

const view = Layout ? () => <Layout>{inner()}</Layout> : inner;

return {
route: props.route,
view,
};
}
Loading