@effectify/react-router 0.1.0 → 0.3.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 CHANGED
@@ -41,15 +41,20 @@ Use the Effect-based loaders and actions in your React Router routes:
41
41
  ```typescript
42
42
  // routes/home.tsx
43
43
  import type * as Route from "./+types.home";
44
- import { Ok, LoaderArgsContext } from "@effectify/react-router";
44
+ import { httpSuccess, httpFailure, LoaderArgsContext } from "@effectify/react-router";
45
45
  import { withLoaderEffect } from "~/lib/server-runtime";
46
- import * as T from "effect/Effect"
46
+ import * as Effect from "effect/Effect"
47
47
 
48
48
  export const loader = withLoaderEffect(
49
- T.gen(function* () {
49
+ Effect.gen(function* () {
50
50
  const { request } = yield* LoaderArgsContext
51
- yield* T.log("request", request)
52
- return yield* T.succeed(new Ok({ data: { hello: 'world' }}))
51
+ yield* Effect.log("request", request)
52
+
53
+ // Improved DX: Simple syntax for success responses
54
+ return yield* httpSuccess({ hello: 'world' })
55
+
56
+ // For error responses, use:
57
+ // return yield* httpFailure("Something went wrong")
53
58
  })
54
59
  )
55
60
 
@@ -63,6 +68,40 @@ export default function Home({ loaderData }: Route.ComponentProps) {
63
68
  }
64
69
  ```
65
70
 
71
+ ## Improved Developer Experience
72
+
73
+ The library provides helper functions for better DX when returning HTTP responses:
74
+
75
+ ### Success Responses
76
+
77
+ Instead of the verbose:
78
+ ```typescript
79
+ return yield* Effect.succeed(new HttpResponseSuccess({ data: { hello: 'world' }}))
80
+ ```
81
+
82
+ Use the simplified syntax:
83
+ ```typescript
84
+ return yield* httpSuccess({ hello: 'world' })
85
+ ```
86
+
87
+ ### Error Responses
88
+
89
+ For error handling, use the `httpFailure` helper:
90
+ ```typescript
91
+ return yield* httpFailure("Something went wrong")
92
+ // or with more complex error objects
93
+ return yield* httpFailure({ code: 'VALIDATION_ERROR', message: 'Invalid input' })
94
+ ```
95
+
96
+ ### Redirects
97
+
98
+ For redirects, use the `httpRedirect` helper:
99
+ ```typescript
100
+ return yield* httpRedirect('/login')
101
+ // or with custom status/headers
102
+ return yield* httpRedirect('/dashboard', { status: 301 })
103
+ ```
104
+
66
105
  ## API
67
106
 
68
107
  ### `make(layers)`
@@ -88,9 +127,49 @@ Effect context providing access to React Router loader arguments including:
88
127
 
89
128
  ### Response Types
90
129
 
91
- - `Ok(data)`: Successful response with data
92
- - `Redirect(url)`: Redirect response
93
- - `Error(message)`: Error response
130
+ - `HttpResponseSuccess<T>(data)`: Successful HTTP response with data
131
+ - `HttpResponseFailure<T>(cause)`: Failed HTTP response with cause
132
+ - `HttpResponseRedirect(to, init?)`: HTTP redirect response
133
+
134
+ ### Helper Functions
135
+
136
+ - `httpSuccess<T>(data: T)`: Creates a successful Effect with HttpResponseSuccess
137
+ - `httpFailure<T>(cause: T)`: Creates a successful Effect with HttpResponseFailure
138
+ - `httpRedirect(to: string, init?: ResponseInit)`: Creates a successful Effect with HttpResponseRedirect
139
+
140
+ ### Error Handling & Logging
141
+
142
+ The library provides comprehensive error handling with full ErrorBoundary support:
143
+
144
+ - **Automatic Error Logging**: Errors are automatically logged using `Effect.logError`
145
+ - **ErrorBoundary Compatible**: Loader errors are properly thrown as `Response` objects for React Router ErrorBoundary
146
+ - **Configurable Logging**: Users can configure their own logging system through Effect layers
147
+ - **Non-blocking**: Logging doesn't block the main thread
148
+ - **Structured Logging**: Errors are logged with context and structured data
149
+ - **Error Preservation**: Original error context is preserved for better debugging
150
+
151
+ ```typescript
152
+ // The library automatically logs errors like this:
153
+ Effect.tapError((cause) => Effect.logError('Loader effect failed', cause))
154
+
155
+ // Loader errors are automatically converted to Response objects for ErrorBoundary:
156
+ // - Effect errors → Response with { ok: false, errors: [...] } and status 500
157
+ // - HttpResponseFailure → Response with { ok: false, errors: [...] } and status 500
158
+ // - Original Response/Error objects are preserved
159
+
160
+ // Users can configure custom logging through Effect layers
161
+ const customLogger = Logger.make(({ message, cause }) => {
162
+ // Custom logging implementation
163
+ console.log(`[${new Date().toISOString()}] ${message}`, cause)
164
+ })
165
+
166
+ const runtime = make(pipe(
167
+ // Your app layers
168
+ MyAppLayer,
169
+ // Custom logger layer
170
+ Logger.replace(Logger.defaultLogger, customLogger)
171
+ ))
172
+ ```
94
173
 
95
174
  ## Requirements
96
175
 
@@ -1,21 +1,33 @@
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";
1
+ import * as Effect from 'effect/Effect';
2
+ declare const HttpResponseSuccess_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> & {
3
+ readonly _tag: "HttpResponseSuccess";
3
4
  };
4
- export declare class Ok<T> extends Ok_base<{
5
+ export declare class HttpResponseSuccess<T> extends HttpResponseSuccess_base<{
5
6
  readonly data: T;
6
7
  }> {
7
8
  }
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";
9
+ declare const HttpResponseFailure_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> & {
10
+ readonly _tag: "HttpResponseFailure";
10
11
  };
11
- export declare class Redirect extends Redirect_base<{
12
+ export declare class HttpResponseFailure<T = unknown> extends HttpResponseFailure_base<{
13
+ readonly cause: T;
14
+ }> {
15
+ }
16
+ declare const HttpResponseRedirect_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> & {
17
+ readonly _tag: "HttpResponseRedirect";
18
+ };
19
+ export declare class HttpResponseRedirect extends HttpResponseRedirect_base<{
12
20
  readonly to: string;
13
21
  readonly init?: number | ResponseInit | undefined;
14
22
  }> {
15
23
  }
16
- export type HttpResponse<T> = Redirect | Ok<T>;
24
+ export type HttpResponse<T> = HttpResponseRedirect | HttpResponseSuccess<T> | HttpResponseFailure<unknown>;
17
25
  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]>>;
26
+ readonly HttpResponseSuccess: (_: HttpResponseSuccess<T>) => any;
27
+ readonly HttpResponseFailure: (_: HttpResponseFailure<unknown>) => any;
28
+ readonly HttpResponseRedirect: (_: HttpResponseRedirect) => any;
29
+ } & { readonly [Tag in Exclude<keyof P, "HttpResponseSuccess" | "HttpResponseFailure" | "HttpResponseRedirect">]: never; }>(fields: P) => (input: HttpResponse<T>) => import("effect/Unify").Unify<ReturnType<P[keyof P]>>;
30
+ export declare const httpSuccess: <T>(data: T) => Effect.Effect<HttpResponseSuccess<T>, never, never>;
31
+ export declare const httpFailure: <T = unknown>(cause: T) => Effect.Effect<HttpResponseFailure<T>, never, never>;
32
+ export declare const httpRedirect: (to: string, init?: number | ResponseInit) => Effect.Effect<HttpResponseRedirect, never, never>;
21
33
  export {};
@@ -1,7 +1,14 @@
1
1
  import * as Data from 'effect/Data';
2
+ import * as Effect from 'effect/Effect';
2
3
  import * as Match from 'effect/Match';
3
- export class Ok extends Data.TaggedClass('Ok') {
4
+ export class HttpResponseSuccess extends Data.TaggedClass('HttpResponseSuccess') {
4
5
  }
5
- export class Redirect extends Data.TaggedClass('Redirect') {
6
+ export class HttpResponseFailure extends Data.TaggedClass('HttpResponseFailure') {
7
+ }
8
+ export class HttpResponseRedirect extends Data.TaggedClass('HttpResponseRedirect') {
6
9
  }
7
10
  export const matchHttpResponse = () => Match.typeTags();
11
+ // Helper functions for better DX
12
+ export const httpSuccess = (data) => Effect.succeed(new HttpResponseSuccess({ data }));
13
+ export const httpFailure = (cause) => Effect.succeed(new HttpResponseFailure({ cause }));
14
+ export const httpRedirect = (to, init) => Effect.succeed(new HttpResponseRedirect({ to, init }));
@@ -1,21 +1,24 @@
1
- import * as T from 'effect/Effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  import type * as Layer from 'effect/Layer';
3
3
  import { type ActionFunctionArgs, type LoaderFunctionArgs } from 'react-router';
4
4
  import { ActionArgsContext, LoaderArgsContext } from '../lib/context.js';
5
5
  import { type HttpResponse } from '../lib/http-response.js';
6
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<{
7
+ withLoaderEffect: <A, B>(self: Effect.Effect<HttpResponse<A>, B, R | LoaderArgsContext>) => (args: LoaderFunctionArgs) => Promise<{
8
8
  ok: true;
9
9
  data: A;
10
10
  } | {
11
11
  ok: false;
12
12
  errors: string[];
13
13
  }>;
14
- withActionEffect: <A, B_1>(self: T.Effect<HttpResponse<A>, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<import("react-router").UNSAFE_DataWithResponseInit<{
14
+ withActionEffect: <A, B_1>(self: Effect.Effect<HttpResponse<A>, B_1, R | ActionArgsContext>) => (args: ActionFunctionArgs) => Promise<import("react-router").UNSAFE_DataWithResponseInit<{
15
15
  ok: false;
16
16
  errors: B_1;
17
17
  }> | {
18
18
  ok: true;
19
19
  response: A;
20
- } | Response>;
20
+ } | import("react-router").UNSAFE_DataWithResponseInit<{
21
+ ok: false;
22
+ errors: string[];
23
+ }> | Response>;
21
24
  };
@@ -1,4 +1,4 @@
1
- import * as T from 'effect/Effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  import * as Exit from 'effect/Exit';
3
3
  import { pipe } from 'effect/Function';
4
4
  import * as Logger from 'effect/Logger';
@@ -9,21 +9,46 @@ import { matchHttpResponse } from '../lib/http-response.js';
9
9
  export const make = (layer) => {
10
10
  const runtime = ManagedRuntime.make(layer);
11
11
  const withLoaderEffect = (self) => (args) => {
12
- const runnable = pipe(self, T.provide(Logger.pretty), T.provideService(LoaderArgsContext, args));
12
+ const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(LoaderArgsContext, args), Effect.tapError((cause) => Effect.logError('Loader effect failed', cause)));
13
13
  return runtime.runPromiseExit(runnable).then(Exit.match({
14
14
  onFailure: (cause) => {
15
- console.error(cause);
16
15
  if (cause._tag === 'Fail') {
17
- throw pipe(cause.error);
16
+ // Preserve the original error for ErrorBoundary
17
+ const error = cause.error;
18
+ if (error instanceof Response) {
19
+ throw error;
20
+ }
21
+ if (error instanceof Error) {
22
+ throw error;
23
+ }
24
+ // Convert other errors to Response for ErrorBoundary with ok: false
25
+ const errorData = { ok: false, errors: [String(error)] };
26
+ throw new Response(JSON.stringify(errorData), {
27
+ status: 500,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ });
18
30
  }
19
- // biome-ignore lint/style/useThrowOnlyError: <library uses non-Error throws>
20
- throw { ok: false, errors: ['Something went wrong'] };
31
+ // Handle other types of failures (interrupts, defects, etc.)
32
+ const errorData = { ok: false, errors: ['Internal server error'] };
33
+ throw new Response(JSON.stringify(errorData), {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
21
37
  },
22
38
  onSuccess: matchHttpResponse()({
23
- Ok: ({ data: response }) => {
39
+ HttpResponseSuccess: ({ data: response }) => {
24
40
  return { ok: true, data: response };
25
41
  },
26
- Redirect: ({ to, init = {} }) => {
42
+ HttpResponseFailure: ({ cause }) => {
43
+ // Convert HttpResponseFailure to Response for ErrorBoundary with ok: false
44
+ const errorMessage = typeof cause === 'string' ? cause : String(cause);
45
+ const errorData = { ok: false, errors: [errorMessage] };
46
+ throw new Response(JSON.stringify(errorData), {
47
+ status: 500,
48
+ headers: { 'Content-Type': 'application/json' },
49
+ });
50
+ },
51
+ HttpResponseRedirect: ({ to, init = {} }) => {
27
52
  redirect(to, init);
28
53
  return { ok: false, errors: ['Redirecting...'] };
29
54
  },
@@ -32,13 +57,16 @@ export const make = (layer) => {
32
57
  };
33
58
  // Don't throw the Error requests, handle them in the normal UI. No ErrorBoundary
34
59
  const withActionEffect = (self) => (args) => {
35
- const runnable = pipe(self, T.provide(Logger.pretty), T.provideService(ActionArgsContext, args), T.match({
60
+ const runnable = pipe(self, Effect.provide(Logger.pretty), Effect.provideService(ActionArgsContext, args), Effect.tapError((cause) => Effect.logError('Action effect failed', cause)), Effect.match({
36
61
  onFailure: (errors) => data({ ok: false, errors }, { status: 400 }),
37
62
  onSuccess: matchHttpResponse()({
38
- Ok: ({ data: response }) => {
63
+ HttpResponseSuccess: ({ data: response }) => {
39
64
  return { ok: true, response };
40
65
  },
41
- Redirect: ({ to, init = {} }) => {
66
+ HttpResponseFailure: ({ cause }) => {
67
+ return data({ ok: false, errors: [String(cause)] }, { status: 400 });
68
+ },
69
+ HttpResponseRedirect: ({ to, init = {} }) => {
42
70
  return redirect(to, init);
43
71
  },
44
72
  }),
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@effectify/react-router",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
+ "description": "Integration of React Router with Effect for React applications",
4
5
  "type": "module",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.js",