Closed drewschrauf closed 1 year ago
I've been playing with another branch (branched from this PR branch) where I've:
Promise.t<Webapi.Fetch.Response.t>
json
to Js.Json.t => Webapi.Fetch.Response.t
patch-package
to allow a custom regex for detecting server modulesThe ergonomics around using Js.Json.t
actually feels much nicer and seems to align with Remix's goal of "use the platform". It's almost like replacing Remix's use of any
with something more analogous to unknown
that requires runtime checks. It also doesn't require making any custom Remix
-focused bindings for the Webapi
module that try and enforce type safety (such as a Remix.Response.t<'appData>
). Obviously this is all at the cost of some compile-time type safety (you're not forcing that loader
returns the same type that's read by useLoaderData
) but the run-time safety should make sure your code will either run with good data or throw with bad data (instead of running with bad data).
@drewschrauf amazing, really appreciate your help here! Sorry I haven't got round to these yet, caught up in a bunch of things atm 😅
Will try to take a look this week 🙇
No worries! I've been tinkering a little less since the cricket season ended anyway 😂
I've put together a port of the jokes app. It's basically all there except the the optimistic UI when creating a new joke. I'm opening this PR as a draft cause I think there are a few issue that are worth figuring out before telling everyone "this is how you do it".
Type safety
You've already done some thinking about this with regards to data loading and KCD opened an issue on the Typescript repo to address some related issues. It's hard to unravel them all to separate talking points but I'll do my best!
Enforcing correct types on route module exports - Route modules have to export the correct functions of the correct types. I feel like this may just be an unavoidable issue when it comes to convention based frameworks and is the same problem that KCD opened the issue for. Rescript has
*.resi
files but how do you ensure that they're using the correct types?There's an option to automatically generate the
*.resi
files using thejs-post-build
hook. That has its own issues though and feels a little heavy handed.Enforcing correct types on loader/action return types - This is the issue you were playing with. The problem is that Remix's return type on loader/action functions is (basically)
Data | Promise<Data> | Response | Promise<Response>
. It would be pretty easy to declare a standard function signature that returnedPromise<Data>
but it turns out that theResponse
versions are used constantly. Within the jokes app, they're used often forredirect
s after form submissions and augmenting standard data responses with additional headers (Vary
, for example). The reliance on web standards is a bit of a curse here asResponse
is clearly not typed.Furthermore, these data types are actually used constantly in remix. For example, the
meta
function gives the user access to theloaderData
to customise the headers.useMatches
gives the user access to not only the current route's data but all parent routes as well. This is generally typed asany
in the Typescript bindings, I assume due to the complexity.I think there's a few options here:
Data()
orResponse()
variant constructors. It makes it easier for the user to return the correct data type but doesn't force it though.Promise.t<Response>
and provide multiplejson
response bindings of typedata => Response
. One for the loader and one for actions. Again, this doesn't force the user to do the right thing but makes it easier.Promise.t<Response>
and create a singlejson
binding of typeJs.Json.t => Response
.Basically, embrace the fact that we don't know what the actual data type is and encourage users to use something like decco to enforce types at runtime. This would mean thatuseLoader
would returnJs.Json.t
and would require decoding at runtime. I actually think this may be the best option given the complexity and how many places attempt to read this data.Server modules on the client
This is a bit of a limitation of Rescript again, similar to the convention-based routing issues we encountered previously. Basically, route modules are a single file containing isomorphic code, as well as code that will only ever run on the server. During development, I think Remix serves up the route modules as-is, meaning that DB dependencies will get bundled for the client. This is a problem when these modules rely on native code that can't get bundled (I ran into issues with
bcrypt
during the port, opting to usebcryptjs
in its place). I'm pretty sure this isn't an issue during production builds as tree-shaking should remove methods likeloader
that would have interacted with the DB.I'm pretty sure this is why Remix's naming convention of
db.server.js
exists. I think this ensures that thedb
module won't get bundled for the client under any circumstances. Unfortunately, using.
in a Rescript module name makes it exotic and impossible to import from another module, makingdb.server.res
kind of useless.I don't think we have any other recourse here except for a change to Remix itself to allow an alternate naming strategy for
client
/server
modules. I haven't looked at whether this is possible, let alone welcome, but it's definitely an issue.I'm keen to hear your thoughts on this!