The Effect Tax
Tags: webdev, javascript, effect, typescript
Published: 2024-05-13 08:36:33

It's been over a year now since I took the effect pill and I'll probably never develop the same way as I did before. I love `effect`, and I am absolutely a power-user.

But at my core, I am a front-end oriented developer. I care about things like bundle size and perceived performance, and I took this into consideration when adopting `effect` into my stack. Although the latter point was never a concern to me with `effect` (I've seen complex, front-end applications running at 120 FPS powered by Effect and React), the former point seemed to be just a fact I'd have to contend with -- `effect` is *large*.

A couple of weeks ago I inherited a `create-react-app` project. It had many of the classic problems seen in medium-to-large React projects - AnyScript, class components, esoteric 1-off dependencies preventing updates, a million lint violations of the `==` kind, multiple overlapping component libraries as well as multiple overlapping styling solutions.

My task was to add four pages to the app as well as a whole bunch of features. Using the existing material was absolutely out of the question - I would have **had** to create my own lib from scratch if I wanted to have any shred of confidence in the app.

I started hacking, and it didn't even take a full hour before I had to wrangle untyped garbage from the legacy side of the fence for a simple HTTP call. Here were some of the problems I found almost immediately: 
- It wasn't parsed (i.e. there was no `zod` or other schema solution)
- It wasn't even typed `Promise<Something>`, it was proudly typed as `Promise<any>` and then just consumed the untyped result
- I wasn't there at the dawn of creation of the app, so I had no idea what the shape of the result even was - I could only follow function names and see the properties being arbitrarily accessed.

So I did research to find out what the shape was supposed to be and created a little utility:

~~~ts
const UnsafeTypeId: unique symbol = Symbol.for("@types/Unsafe");

/**
 * Brands `A` as an unsafe type.
 * An unsafe type is a type that was cast but never validated.
 *
 * @example
 * ```ts
 * const response = await axios.get(...)
 *
 * const data = response.data as Unsafe<Todo>
 * ```
 *
 * @author Datner<contact@datner.me>
 */
export type Unsafe<A> = A & {
  readonly [UnsafeTypeId]?: "Unsafe";
};
~~~

Now I can at least give a proper shape to the result while also acknowledging to future developers that the shape of the type wrapped by `Unsafe` is not actually validated. 

Then I encountered another situation that prompted me to implement a different utility ... and then another ... and then *another* ... and on and on it went. I found myself implementing infrastructure and utilities just so I would have the basic essentials to write usable software.

First I tried just coping with it, then I tried to just let go and get the project done, but these issues continued to linger in the back of my mind ... and then it dawned on me - I was just re-implementing `effect`.

Worse, I'm re-implementing `effect` by trying to glue libraries together that were never designed to work with one another. 

Oh the axios client needs the current tenant? Ok, then it needs access to the context. But it can change in the lifetime of the application and also needs the bearer token, but it might need revalidation or even a re-log. Ah, but I make too many requests for the same thing so I also need some caching. It's been nearly a month now and I've only been building infrastructure! The boring kind too! Plus I hate the result and it's full of bugs I won't see until they pop up in production!

You gotta understand, `effect` does not start and end with `Effect` or any of the other fancy modules like `Stream`, `Schema`, or `PubSub`. It's packed full of useful toolbelt utils like you'll get with `lodash`, but with full support and interop with each other and ofc everything strictly typed.

So I gave up. Whatever the `effect` "tax" is, it's worth it. From what we've seen, the "tax" on bundle size is about 50kb. Depending on your use-case, this may seem like a lot or very little. But considering that I added around 20k LOC to the project, as well as some new dependencies, for me it wasn't even a consideration.

```sh
$ npm i effect @effect/schema @effect/platform @effect/platform-browser
```


I began replacing all my imitations with the utils from `effect`. 

To give a visual example, here is a `combine` function I created for a particular `useQueries` query from the great `@tanstack/react-query` (a dependency which I also added a week ago).

```ts
import { Array, Predicate, Option, pipe } from "effect"

// further down....
const combine = (
  queries: [
    UseSuspenseQueryResult<User[], Error>,
    UseSuspenseQueryResult<Recipient[], Error>,
  ],
) => {
  const [users, recipients] = queries;

  const initialRows = pipe(
    users.data,
    Array.filter((_) => Option.isSome(_.emailAddress)),
    Array.appendAll(recipients.data),
    Schema.decodeSync(Schema.Array(NormalizedRow)),
    Array.dedupeWith((a, b) => a.Email === b.Email && a.fromIDP),
  );

  const isRecipient = Predicate.or(RecipientRow.isRecipient, (_) =>
    recipients.data.some((r) => r.recepientId === _.id),
  );
  return {
    pending: queries.some((_) => _.isPending),
    initialRows,
    initialSelected: Array.filterMap(initialRows, (_) =>
      isRecipient(_) ? Option.some(_.id) : Option.none(),
    ),
  };
};
```

This function takes the result of two collections that have some possible overlap, cleans up `users` from members that don't have an email, concatenates it to the `recipients` into a nice wholesome `(User | Recipient)[]` that is unusable, uses an `@effect/schema` `NormalizedRow` schema to homogenize it into an actually useful `RecipientRow[]`, and remove duplicate rows, keeping only the `User`-originated ones.

This utility also then creates a derived collection that is just the ids of the `RecipientRows` that either represent a `Recipient` *or* represent a `User` *that has a corresponding `Recipient`* (remember we deduped them).

You could glue a solution that does this using `lodash`, `zod`, and some hand-crafted stuff, but it won't be this elegant considering the definition above. Especially the normalization part and the types. In this snippet it looks like it's trivial -- it's not. This function was over 100 LOC before.

Recently, I have seen `effect` being referred to as "a different language" or "hard to learn" or "unreadable", including commentary from people who have never even used the library. Effect is a complete toolkit for developing enterprise-grade applications. Not an RxJS simulator or a cool way to use `Result` from Rust.

I also have a ton of `Effect`-effect usage. With all the bells and whistles! From a quick search here's a partial list of modules I use in this project

- `Effect`
- `Layer`
- `Context`
- `Predicate`
- `Array`
- `Cause`
- `Option`
- `Config`
- `Scope`
- `ManagedRuntime`
- `GlobalValue`
- `String`
- `HttpClient`
- `ClientRequest`
- `ClientResponse`
- `Schema`
- `Data`

> "Oh no! Egad! Thats too much to learn!" - _You, right now, totally ignoring that you probably have learned all of these concepts, and much more, piecemeal, from scratch on every project according to whatever 30-or-so packages are included in the project_

Don't worry dear reader, you don't have to learn any of these, you can be productive
with `effect` even when using just a single module, and learn to use the rest at your own pace. For me its the missing standard library that is internally consistent and 100% interoperable.

Cut to yesterday, I've finally finished implementing all the features I needed. 80 changed files (mostly new files), around 12K LOC added by yours truly (trimmed a lot of fat thanks to `effect`, but react theres still a lot of React code and CSS).

So, nows the money time. How much did I pay for my hubris?

![net 50kb difference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lj5izxw40x8wlzza7mlj.png)

)

Net 50kb.

`effect`, `@effect/schema`, `@effect/platform`, `@effect/platform-browser`, and `react-query` to boot.

80 files, 4 new routes, nearly all new code.

This was not an isolated case, as `effect` makes its way into more and more areas of your application, its size amortizes because you inevitably trim a lot of "fat" from your application. I removed almost all the custom utilities I had previously built and was able to uninstall several packages.

For example, my production server clocks in at just under 90kb.

Grief and effort was saved from implementing and fixing bugs in tools that I get for free just by using `effect`.

Additionally, `effect` is 100% tree-shakeable, so I didn't end up with unnecessary code in my final application bundle.

Like MooTools, knockoutjs, jquery, express, angularjs, react, redux, nextjs, rxjs, and many other powerful abstraction that changed how we write JavaScript code, I can say without hesitation that the `effect` "tax" was 100% worth it.