dev-resources.site
for different kinds of informations.
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 asPromise<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<[email protected]>
*/
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.
$ 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).
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.
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.
Featured ones: