Skip to content

Instantly share code, notes, and snippets.

@aleclarson
Last active November 7, 2025 19:33
Show Gist options
  • Save aleclarson/c57da75cd6e7cbfd2da34e5f7a367669 to your computer and use it in GitHub Desktop.
Save aleclarson/c57da75cd6e7cbfd2da34e5f7a367669 to your computer and use it in GitHub Desktop.
Remix 3 `this.props` proposal

“Setup props” considered harmful!

Ryan Florence doesn't like me saying that, but it's true.

Why is it true? Well, they look like render props until you scroll down to the return statement and see an arrow function. If you refactor a stateless component into a stateful component (i.e. add a setup scope), the render props subtly become setup props, right under your nose.

Most of the time, using setup props is dangerous and unnecessary, as it leads to problems down the road. At first, all is well, but then you use the component more dynamically and, BOOM, you've got a stale data issue that presents itself in confusing ways. Of course, this problem is most apparent in more complex applications with many moving parts.

OK, let's say there's a problem. Can it be avoided?

More importantly, can it be avoided without sacrificing the aesthetic appeal of Remix 3's component design? Actually, yes! At least, that's my opinion.

// Before (current API)
function MyComponent(this: Remix.Handle<Context>, props: Props) {
  props; // Props are fresh during setup. Captured by default.

  return (props: Props) => {
    props; // Props are fresh during render. Optionally captured.
  };
}

// After (proposed API)
function MyComponent(this: Remix.Handle<Props, Context>) {
  this.props; // Props are always fresh. Optionally captured.

  // Capture props when necessary.
  const { defaultValue } = this.props;

  return (props: Props) => {
    props; // Props are fresh during render. Optionally captured.
    this.props; // Props are always fresh. Optionally captured.
  };
}

The key difference

Notice how, in the new version, all references to props or this.props have the “Optionally captured” description. This is the key difference. Staleness is never the default. It's always something you have to opt into.

Additionally, when you need to hoist shared state up to a parent component that was once a stateless layout component, there's no longer a footgun that results in stale props.

TypeScript will nudge you to refactor your component such that any existing props references still point to the latest props, as you were doing before the refactor. This encourages the developer to refactor their component, not by wrapping the JSX return expression in an arrow function, but by wrapping the stateless component in a stateful component. This point is subtle, but important.

type Props = { defaultValue?: string }
function MyComponent(this: Remix.Handle<Props, Context>) {
this.props // Props are always fresh. Optionally captured.
// Capture props when necessary.
const { defaultValue } = this.props
return (props: Props) => {
props // Props are fresh during render. Optionally captured.
this.props // Props are always fresh. Optionally captured.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment