New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add pattern matching #42
Comments
Hey @waspeer ! We used to call this lib remix-domains but we realized it is not specific to remix at all thus we renamed it. We are considering moving the Request/Response APIs to a complimentary lib btw. Thank you for your comment! |
One additional comment: if you move the pattern matching to inside a domain function, when you |
@gustavoguichard Sounds good! Let me know if I can help out at all. @danielweinmann Good point! In my example I was imagining the |
Hey @waspeer make sure you check the proposed // domain/projects.server.ts
const updateProjectName = makeDomainFunction( z.object({ action: z.literal('update') }))(async () => {})
const deleteStuff = makeDomainFunction( z.object({ action: z.literal('delete') }))(async () => {})
// in your route
export const action: ActionFunction = async ({ request }) => {
const result = await first(updateProjectName, deleteStuff)(await inputFromForm(request))
if (!result.success) throw redirect('/error')
return json(result.data)
}; The benefit here is that our proposed pattern for the loader/actions are kept and the composition won't break ;) |
That works for me :)
Out of curiousity, could you explain this a bit more? Just curious about your design process. |
Sure @waspeer! We like to keep our Remix Actions and Loaders all in the same shape: export async function loader({ request }: LoaderArgs) {
const result = await someDomainFunction(input, env)
if (!result.success) throw notFound() // or something else
return json(result.data)
} And we try to solve all our problems with composition of domain functions so we have one domain function per loader/action. This allows us to create Response helpers such as: const loaderResponse = <T extends Result<X>, X>(
result: T,
): T extends SuccessResult<X> ? TypedResponse<X> : TypedResponse<ErrorResult> =>
result.success
? json(result.data, { status: 200 })
: json(result, { status: 404 })
const loaderResponseOrThrow = <T extends Result<X>, X>(
result: T,
): T extends SuccessResult<X> ? TypedResponse<X> : never => {
if (!result.success) throw internalError(result.errors[0]?.message)
return json(result.data, { status: 200 }) as any
}
const loaderResponseOrRedirect = <T extends Result<unknown>>(
result: T,
redirectPath: string,
): T extends { data: infer X } ? TypedResponse<X> : never => {
if (!result.success) throw redirect(redirectPath)
return json(result.data, { status: 200 }) as any
} Which leads us to tiny and similar actions/loaders throughout the app: // app/domain/my-domain.server.ts
const someDomainFunction = merge(dfA, dfB)
// app/routes/foo.tsx
export async function loader({ request }: LoaderArgs) {
const result = await someDomainFunction(
await someInputFunction(request),
await someEnvFunction(request)
)
return loaderResponseOrRedirect(result, '/homepage')
} By solving all our data needs with domain-function composition we can also properly type our components when needed: // app/domain/foo.server.ts
const dfA = makeDomainFunction(z.object({}))(async () => ({ resultA: 'foo' }))
const dfB = makeDomainFunction(z.object({}))(async () => ({ resultB: 'foo' }))
const getFooData = merge(dfA, dfB)
// app/routes/foo.tsx
export async function loader({ request }: LoaderArgs) {
return loaderResponseOrThrow(await getFooData({}))
}
export default () => {
const data = useLoaderData<typeof loader>()
return (
<div>
<MyFooComponent data={data} />
<AComponent resultA={data.resultA} />
<BComponent resultB={data.resultB} />
</div>
}
// app/ui/my-foo-component.tsx
type Props = UnpackData<typeof getFooData>
function MyFooComponent({ resultA, resultB }: Props) {
// ...
}
// app/ui/a-component.tsx
type Props = Pick<UnpackData<typeof getFooData>, 'resultA'>
function AComponent({ resultA }: Props) {
// ...
}
// app/ui/b-component.tsx
type Props = Pick<UnpackData<typeof getFooData>, 'resultB'>
function BComponent({ resultB }: Props) {
// ...
} This is a little bit about how we've been using the I hope it helps clarifying where we are heading to! |
Thanks for taking the time to elaborate! Makes a lot of sense :) |
I'll close it for now but we are open to discuss it again in the future as pattern matching is 🎯🔥 |
Hi!
I was just about to build something like this myself when I stumbled on this library haha. Great work! I love how you approached it.
A common pattern in Remix when having multiple forms on one page, is to add a hidden input named
action
to your form. It would be great to be able to combine domain functions with pattern matching to handle this. For example:ts-pattern
immediately comes to mind. Maybe this could be integrated or serve as inspiration.Also, quick question; is this library actually specific to Remix? By the looks of it I can use this in any other framework, as long as it gets a
Request
as input?The text was updated successfully, but these errors were encountered: