question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add pattern matching

See original GitHub issue

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:

type Input =
  | {
      action: 'update';
      name: string;
    }
  | {
      action: 'delete';
      id: string;
    };

// Or possibly:
// type Input =
//   | UnpackInput<typeof updateProjectName>
//   | UnpackInput<typeof deleteStuff>;

export const action: ActionFunction = async ({ request }) => {
  const input = (await inputFromForm(request)) as Input;

  return match(input.action)
    .with({ action: 'update' }, (input) => updateProjectName(input))
    .with({ action: 'delete' }, (input) => deleteStuff(input))
    .otherwise(() => {
      throw new Error('Unexpected action');
    });
};

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?

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:8 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
waspeercommented, Oct 19, 2022

Thanks for taking the time to elaborate! Makes a lot of sense 😃 As far as I’m concerned we can close this issue, unless you’re still thinking about implementing pattern matching.

1reaction
gustavoguichardcommented, Oct 19, 2022

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 domain-functions library at Seasoned. I also wrote a post about it although it is a bit dated as we’ve found more patterns and added more API surface to the library… it still worth a read to understand some of our reasoning though 😉

I hope it helps clarifying where we are heading to! Cheers

Read more comments on GitHub >

github_iconTop Results From Across the Web

Pattern matching overview - C# guide | Microsoft Learn
C# pattern matching provides more concise syntax for testing expressions and taking action when an expression matches. The " is expression" ...
Read more >
Pattern Matching | Tour of Scala - Scala Documentation
Pattern matching is a mechanism for checking a value against a pattern. A successful match can also deconstruct a value into its constituent...
Read more >
PEP 636 – Structural Pattern Matching: Tutorial
This PEP is a tutorial for the pattern matching introduced by PEP 634. ... For example, you might want to add single verbs...
Read more >
Pattern Matching - Reason
Pattern matching provides a way to conditionally execute code when the shape of some data matches a particular pattern. It is similar to...
Read more >
GitHub - tc39/proposal-pattern-matching
This proposal adds a pattern matching expression to the language, based in part on the existing Destructuring Binding Patterns. This proposal was approved ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found