Open cromoteca opened 1 week ago
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 94.91%. Comparing base (
20b0aa0
) to head (06bafdc
).
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
I'm a bit confused by the terminology here. I understand "loader" as a mechanism that ensures data needed by a view is loaded in parallel with the script needed for rendering the view. That cannot be the case here since the FS router doesn't even support lazy loading views. There's also a minor benefit from starting the request before running the view's render function but should be a very minimal impact in typical cases.
The terminology is borrowed from the React Router loader functionality that is used here.
The benefits I can see are: avoid the need to use useEffect
and the possibility to react to errors directly in the router, for example to return a 404 if /users/123
is not found.
I'm a little hesitant about the satisfies ViewConfig
syntax and I'm very much hesitant about the total lack of type safety when the service method takes parameters.
Would it be possible to come up with a syntax that avoids those issues? Doing that would probably somehow require a separate definition rather than building on top of export const config
.
To avoid satisfies
, a separate declaration is needed. Concerning type safety, I did an experiment with a @Loader
annotation in Java, that generates different code in TypeScript allowing for parameter names to be associated by name instead of order. But, as parameters are always strings, either also server parameters are strings (which is not practical), or we accept that a conversion from strings will be attempted.
Type conversion of parameters is indeed also a concern but my main concern was that it's only based on matching the order without any reference to parameter names.
I wonder if there's some synergy with the concept that I started investigating related to adding type safety to FS route parameters? This would make the parameters more explicit in general and it would also have an option for defining parameter types. I wonder if something similar could also be used to map route parameters to loaders?
Sure! If we have type safety in parameters, that could be matched to server methods using some mechanism like the @Loader
annotation I was talking about.
I don't think a Java annotation would be very useful here since we're not generating Java code. We could just as well let every endpoint method be eligible to be used as a loader while hooking things up from TS code that can reference generated TS code.
If we assume that we generate a model based on the view's route parameters, then we could hook things up using something like loader(HelloEndpoint.repeat, model.text, model.times)
assuming the type system can be abused in that way. Otherwise, there's the option of generating an entry point on all endpoint methods to enable use like HelloEndpoint.repeat.asLoader(model.text, model.times)
.
There might still be one step that isn't type-safe since the model (or more specifically its type) has to come from somewhere but that's something we might not be able to avoid.
Types could be specified in the config as generics. That would give something like:
export const config: ViewConfig<{ text: string, times: number }> = {
title: "Repeat",
loader: HelloEndpoint.repeat,
};
The idea of using an annotation was to change how code is generated for those annotated methods. So, a method like
@Loader
public User getUser(long id) throws UserNotFoundException {
...
}
would be generated as
async function getUser({ params }: LoaderFunctionArgs): Promise<User> {
return client.call('LoadersEndpoint', 'getUser', { id: Number(params.id) });
}
where LoaderFunctionArgs
is the type used by React Router and UserNotFoundException
extends our EndpointException
.
Issues
0 New issues
0 Accepted issues
Measures
0 Security Hotspots
0.0% Coverage on New Code
0.0% Duplication on New Code
I think it's problematic to generate @Loader
methods in a different way since you might also want to use that method with parameters that are not derived from the URL. One obvious example is lazy loading of data - you wouldn't want to update the URL at the moment when the Grid component determines that more items need to be fetched but you would still want to use a loader for the initially fetched items. That's why I think it would be better to aim for a design where a regular endpoint method can also be used as a loader.
In other words, what I'm leaning towards is something like TanStack Query (previously known as React Query) that is also used as a loader. Initial loading is triggered before the view is rendered based on router parameters and default values for any additional state. Subsequent updates to route parameters would update the URL whereas updates to other parameters would "only" trigger a re-fetch (and maybe also pushState
?). Errors from the initial load could be handled as route errors (e.g. 401 or 404) whereas other errors (e.g. server timeout) would be handled by the view.
If we want to also enable the waterfall busting benefit in combination with code splitting (i.e. lazy loaded view implementations), then the loader definition along with anything it depends on would have to be defined outside the actual view module. We can lift out immutable values from the view module at compile time but we might not want to do anything like that for e.g. arrow functions. This would put some additional constraints on the design even though we might also want to provide a simpler mode of operation that could have everything in a single file at the cost of a waterfall when combined with code splitting.
This PR is an attempt to allow File Router users to define a server method as data loader. Here's how to use it. Let's say we have a service with two methods (using
@NonNullApi
):The first method can be used as such:
It is necessary to use
satisfies ViewConfig
instead of: ViewConfig
so thatuseLoader
can infer the right return type.The second server method has parameters, so it's expected to be used in a route like
/repeat/{text}/{times}
. The name of the parameters is not important, it's the order that matters.In this case,
repeated
is astring
. Calling/repeat/abc/3
will printabcabcabc
in the view.Server errors can potentially be handled at router level, allowing for example to return a 404 in case of user id not found.
Tests are missing: consider this PR as a base of discussion.