@effectify/react-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @effectify/react-router
2
+
3
+ Integration of [React Router](https://reactrouter.com/) with [Effect](https://effect.website/) for React applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install @effectify/react-router
10
+
11
+ # yarn
12
+ yarn add @effectify/react-router
13
+
14
+ # pnpm
15
+ pnpm add @effectify/react-router
16
+
17
+ # bun
18
+ bun add @effectify/react-router
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ### 1. Setup Server Runtime
24
+
25
+ Create a server runtime with your Effect layers:
26
+
27
+ ```typescript
28
+ // lib/server-runtime.ts
29
+ import { make } from "@effectify/react-router";
30
+ import * as Layer from "effect/Layer"
31
+
32
+ const layers = Layer.empty
33
+
34
+ export const { withLoaderEffect, withActionEffect } = make(layers)
35
+ ```
36
+
37
+ ### 2. Use in Route Components
38
+
39
+ Use the Effect-based loaders and actions in your React Router routes:
40
+
41
+ ```typescript
42
+ // routes/home.tsx
43
+ import type * as Route from "./+types.home";
44
+ import { Ok, LoaderArgsContext } from "@effectify/react-router";
45
+ import { withLoaderEffect } from "~/lib/server-runtime";
46
+ import * as T from "effect/Effect"
47
+
48
+ export const loader = withLoaderEffect(
49
+ T.gen(function* () {
50
+ const { request } = yield* LoaderArgsContext
51
+ yield* T.log("request", request)
52
+ return yield* T.succeed(new Ok({ data: { hello: 'world' }}))
53
+ })
54
+ )
55
+
56
+ export default function Home({ loaderData }: Route.ComponentProps) {
57
+ return (
58
+ <div>
59
+ <h1>Home</h1>
60
+ <pre>{JSON.stringify(loaderData.data, null, 2)}</pre>
61
+ </div>
62
+ )
63
+ }
64
+ ```
65
+
66
+ ## API
67
+
68
+ ### `make(layers)`
69
+
70
+ Creates Effect-based runtime helpers for React Router.
71
+
72
+ #### Parameters
73
+
74
+ - `layers`: Effect Layer containing your application services and dependencies.
75
+
76
+ #### Returns
77
+
78
+ An object containing:
79
+ - `withLoaderEffect`: Wrapper for React Router loaders using Effect
80
+ - `withActionEffect`: Wrapper for React Router actions using Effect
81
+
82
+ ### `LoaderArgsContext`
83
+
84
+ Effect context providing access to React Router loader arguments including:
85
+ - `request`: The incoming Request object
86
+ - `params`: Route parameters
87
+ - `context`: Additional context data
88
+
89
+ ### Response Types
90
+
91
+ - `Ok(data)`: Successful response with data
92
+ - `Redirect(url)`: Redirect response
93
+ - `Error(message)`: Error response
94
+
95
+ ## Requirements
96
+
97
+ - React Router v7+
98
+ - Effect ecosystem (`effect`)
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,3 @@
1
+ export * from './lib/context.js';
2
+ export * from './lib/http-response.js';
3
+ export * as Runtime from './lib/runtime.js';
@@ -0,0 +1,3 @@
1
+ export * from './lib/context.js';
2
+ export * from './lib/http-response.js';
3
+ export * as Runtime from './lib/runtime.js';
@@ -0,0 +1,9 @@
1
+ import * as Context from 'effect/Context';
2
+ import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
3
+ declare const ActionArgsContext_base: Context.TagClass<ActionArgsContext, "ActionArgsContext", ActionFunctionArgs<any>>;
4
+ export declare class ActionArgsContext extends ActionArgsContext_base {
5
+ }
6
+ declare const LoaderArgsContext_base: Context.TagClass<LoaderArgsContext, "LoaderArgsContext", LoaderFunctionArgs<any>>;
7
+ export declare class LoaderArgsContext extends LoaderArgsContext_base {
8
+ }
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import * as Context from 'effect/Context';
2
+ export class ActionArgsContext extends Context.Tag('ActionArgsContext')() {
3
+ }
4
+ export class LoaderArgsContext extends Context.Tag('LoaderArgsContext')() {
5
+ }
@@ -0,0 +1,21 @@
1
+ declare const Ok_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
2
+ readonly _tag: "Ok";
3
+ };
4
+ export declare class Ok<T> extends Ok_base<{
5
+ readonly data: T;
6
+ }> {
7
+ }
8
+ declare const Redirect_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
9
+ readonly _tag: "Redirect";
10
+ };
11
+ export declare class Redirect extends Redirect_base<{
12
+ readonly to: string;
13
+ readonly init?: number | ResponseInit | undefined;
14
+ }> {
15
+ }
16
+ export type HttpResponse<T> = Redirect | Ok<T>;
17
+ export declare const matchHttpResponse: <T>() => <P extends {
18
+ readonly Ok: (_: Ok<T>) => any;
19
+ readonly Redirect: (_: Redirect) => any;
20
+ } & { readonly [Tag in Exclude<keyof P, "Ok" | "Redirect">]: never; }>(fields: P) => (input: HttpResponse<T>) => import("effect/Unify").Unify<ReturnType<P[keyof P]>>;
21
+ export {};
@@ -0,0 +1,7 @@
1
+ import * as Data from 'effect/Data';
2
+ import * as Match from 'effect/Match';
3
+ export class Ok extends Data.TaggedClass('Ok') {
4
+ }
5
+ export class Redirect extends Data.TaggedClass('Redirect') {
6
+ }
7
+ export const matchHttpResponse = () => Match.typeTags();
@@ -0,0 +1,21 @@
1
+ import * as T from 'effect/Effect';
2
+ import type * as Layer from 'effect/Layer';
3
+ import { type ActionFunctionArgs, type LoaderFunctionArgs } from 'react-router';
4
+ import { ActionArgsContext, LoaderArgsContext } from '../lib/context.js';
5
+ import { type HttpResponse } from '../lib/http-response.js';
6
+ export declare const make: <R, E>(layer: Layer.Layer<R, E, never>) => {
7
+ withLoaderEffect: <A, B>(self: T.Effect<HttpResponse<A>, B, R | LoaderArgsContext>) => (args: LoaderFunctionArgs) => Promise<{
8
+ ok: true;
9
+ data: A;
10
+ } | {
11
+ ok: false;
12
+ errors: string[];
13
+ }>;
14
+ withActionEffect: <A, B_1>(self: T.Effect<HttpResponse<A>, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<import("react-router").UNSAFE_DataWithResponseInit<{
15
+ ok: false;
16
+ errors: B_1;
17
+ }> | {
18
+ ok: true;
19
+ response: A;
20
+ } | Response>;
21
+ };
@@ -0,0 +1,49 @@
1
+ import * as T from 'effect/Effect';
2
+ import * as Exit from 'effect/Exit';
3
+ import { pipe } from 'effect/Function';
4
+ import * as Logger from 'effect/Logger';
5
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
6
+ import { data, redirect } from 'react-router';
7
+ import { ActionArgsContext, LoaderArgsContext } from '../lib/context.js';
8
+ import { matchHttpResponse } from '../lib/http-response.js';
9
+ export const make = (layer) => {
10
+ const runtime = ManagedRuntime.make(layer);
11
+ const withLoaderEffect = (self) => (args) => {
12
+ const runnable = pipe(self, T.provide(Logger.pretty), T.provideService(LoaderArgsContext, args));
13
+ return runtime.runPromiseExit(runnable).then(Exit.match({
14
+ onFailure: (cause) => {
15
+ console.error(cause);
16
+ if (cause._tag === 'Fail') {
17
+ throw pipe(cause.error);
18
+ }
19
+ // biome-ignore lint/style/useThrowOnlyError: <library uses non-Error throws>
20
+ throw { ok: false, errors: ['Something went wrong'] };
21
+ },
22
+ onSuccess: matchHttpResponse()({
23
+ Ok: ({ data: response }) => {
24
+ return { ok: true, data: response };
25
+ },
26
+ Redirect: ({ to, init = {} }) => {
27
+ redirect(to, init);
28
+ return { ok: false, errors: ['Redirecting...'] };
29
+ },
30
+ }),
31
+ }));
32
+ };
33
+ // Don't throw the Error requests, handle them in the normal UI. No ErrorBoundary
34
+ const withActionEffect = (self) => (args) => {
35
+ const runnable = pipe(self, T.provide(Logger.pretty), T.provideService(ActionArgsContext, args), T.match({
36
+ onFailure: (errors) => data({ ok: false, errors }, { status: 400 }),
37
+ onSuccess: matchHttpResponse()({
38
+ Ok: ({ data: response }) => {
39
+ return { ok: true, response };
40
+ },
41
+ Redirect: ({ to, init = {} }) => {
42
+ return redirect(to, init);
43
+ },
44
+ }),
45
+ }));
46
+ return runtime.runPromise(runnable);
47
+ };
48
+ return { withLoaderEffect, withActionEffect };
49
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@effectify/react-router",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "@effectify/source": "./src/index.ts",
14
+ "types": "./dist/src/index.d.ts",
15
+ "import": "./dist/src/index.js",
16
+ "default": "./dist/src/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "!**/*.tsbuildinfo"
22
+ ],
23
+ "dependencies": {
24
+ "@effect/platform": "0.91.1",
25
+ "@effect/platform-node": "0.97.1",
26
+ "effect": "3.17.14",
27
+ "react-router": "7.9.2"
28
+ },
29
+ "devDependencies": {},
30
+ "peerDependencies": {},
31
+ "optionalDependencies": {}
32
+ }