DEV Community

Datner
Datner

Posted on

The Effect Tax

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:

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";
};
Enter fullscreen mode Exit fullscreen mode

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.

$ npm i effect @effect/schema @effect/platform @effect/platform-browser
Enter fullscreen mode Exit fullscreen mode

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).

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(),
    ),
  };
};
Enter fullscreen mode Exit fullscreen mode

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

)

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.

Top comments (12)

Collapse
 
brense profile image
Rense Bakker

The difference with the other article I just read couldn't be bigger. This article is actually informative and indept and not written by AI, but it gets almost zero response :(
Anyways, good job πŸ‘ and I hope you get more readers and reactions.

Collapse
 
datner profile image
Datner

Thank you for the kind words!

Collapse
 
kyrregjerstad profile image
Kyrre Gjerstad

Thanks for sharing! I've been trying out effect for the last couple of weeks. Still trying to make up my mind if this is something for me, but your post has helped me see some practical use-cases for Effect

Collapse
 
datner profile image
Datner

Effect can basically do everything besides rendering and intensive number crunching, not saying it should, but I never need to search for a use-case effect covers, I just have use cases. Like a regular application, except I enjoy the experience lol

Collapse
 
hinogi profile image
Stefan Schneider

I use Vue and Nestjs and I think there are some major overlaps with effect conceptually, like service and context on in nestjs. They are not compatible I think but since they seem so similar I am always asking myself, why would I write this in effect if the framework brings it with it. That is my personal hinderance to use effect or to try to convince my team to use it.
Also, there are not enough examples for frontend and for graphql usage as well as a lot of tooling that autogenerates types on basis of a graphql schema is not available.
I am still very much interested in effect, but since I do not have a project that could make use of it, and I don't know how to beneficially use it in my current work project, I'm stuck.
With no prior FP knowledge the learning curve is even steeper I guess.

Collapse
 
datner profile image
Datner

effects resources and recommend patterns are still emerging, if you're not comfortable adopting something at this stage thats totally valid! You can still play with effect on the side or use it for smaller projects until you feel it's ready.
If you were able to read the little code I have in my article, that's is rough the sum of required knowledge to be effective with effect, so don't worry about it

Collapse
 
hinogi profile image
Stefan Schneider

I still have to figure out what applications of effect code is out of bounds like "how does it behave with reactivity(refs/effects) from vue?" "how do I come back from effect land to vue land?" "Do I just reinvent the wheel by using effect when there is something in vue already?" "Does vue maybe even do something similar under the hood like with the similarity to signals?"

Collapse
 
karaca19 profile image
Bora Karaca

Thanks for sharing. Effect has been on my radar for quite some time now but I wanted see to a practical example of it. This was helpful. Effect looks promising.

Collapse
 
gunslingor profile image
gunslingor

Welcome to my world Mr. Author, lololololollllll.

Collapse
 
datner profile image
Datner

I am not sure what you mean πŸ˜ƒ

Collapse
 
gunslingor profile image
gunslingor

Only that I have the same experience... most apps, hell every app, I've seen in the web world is like this... pure implementation and zero engineering. Dependency conflicts are a death sentence and can never be allowed, first rule of any development going back 50 years... yet, every big app I've seen has them. It's rediculous. Only in web development have I seen this level of chaos.

Thread Thread
 
datner profile image
Datner

Yes I'm well acquainted with the issue, I've been developing applications for over a decade now πŸ˜„
Thats why I instantly understood why effect is a big deal