Skip to content

Design preview: Stateless Functional Components in JSX #5478

Closed
@RyanCavanaugh

Description

@RyanCavanaugh

Pre-reading

JSX in TypeScript refresher: #3203
Original issue: #4861
Announcement: https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html
Docs: https://facebook.github.io/react/docs/reusable-components.html#stateless-functions
Intro: https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.d6c0m2mhd
Source: https://github.com/facebook/react/blob/0ef5112e892b2350756400c1d047a2213002bd7d/src/renderers/shared/reconciler/ReactCompositeComponent.js#L155

Detecting Stateless Functional Components (SFCs)

SFCs are invoked with three arguments: props, context, and updater. These are the same three arguments passed to component class constructors, so the parameter information is not useful in distinguishing SFCs from classes.

SFCs are invoked without new whereas classes are invoked with new. However, some older React components ignore the fact that they are invoked that way, and may exist with just call signatures, so call vs construct signatures are not useful for SFC detection either.

We currently require that the tag expression used in an <Element /> expression resolve to something with a call or construct signature whose return type is assignable to JSX.ElementClass. React expects that the result of an invocation of an SFC is the same type that we call JSX.Element. So we can use the return type of call signatures of three or fewer parameters to distinguish SFCs from classes.

Our previous JSX typing design tried to fall back to reasonable behavior when there was only minimal type information in the JSX namespace. When both JSX.Element and JSX.ElementClass are empty, however, SFC detection becomes much harder. I propose that a component is only an SFC if it is assignable to JSX.Element and not assignable to JSX.ElementClass -- this preserves our good fallback behavior when there isn't a solid react.d.ts present, without too much additional complexity.

The Element Attributes Type for SFCs

Determining the element attributes type for a SFC is simple: it's the type of the first parameter to the function, or the empty type if the function has zero parameters. However, keep reading.

Resolving the ref and key madness in props types

Current State

TypeScript with React has very annoying problems around the ref and key attributes.

Problem A: the ref and key attributes are visible when consuming a class, e.g. <Element key="foo" /> is always valid, but the key is never visible inside the class, e.g. this.props.key is never defined in the class render method. This is only slightly annoying as it's hard to use them accidentally in any meaningful fashion.

Problem B: More seriously, it's very easy as a component author to make a component whose props type lacks the key and ref properties, even though those properties should always exist. Because these attributes are optional on JSX elements, we can't even enforce this restriction through the type system.

Problem C: Finally, SFCs do not have a ref attribute (because there's no instance to point to). This is less serious than problems A and B, but is still a hole.

Problem D: The big one. It is valid (and common) to put a key attribute on an SFC-based element, but SFCs are never going to bother defining key themselves in their parameter type:

function Greeter({name = 'world'}) {
  return <div>Hello, {name}!</div>;
}
let names = ['Alice', 'Bob'];
// Error, no property 'key' exists ... !?
let x = <div>{names.map((n, i) => <Greeter name={n} key={i.toString()} /></div>;

Proposal

The proposed solution is to add two new types to the JSX namespace: IntrinsicAttributes and IntrinsicClassAttributes:

    interface IntrinsicAttributes {
        key?: string;
    }

    interface IntrinsicClassAttributes<T> {
        ref?: string|((elem: T) => void);
    }

When determining the element attributes type (the type that determines the allowed attributes of an element), we would intersect the existing type (as determined by the current algorithm) with the properties of either both interfaces (for intrinsic and class-based elements) or just IntrinsicAttributes (for SFCs). IntrinsicClassAttributes<T> would be instantiated with the class instance type for T.

This solves problems A, B, C, and D: it becomes impossible to forget to define ref and key, it becomes an error to access ref or key from this.props, and it becomes an error to attempt to define a ref attribute when writing a JSX element with a SFC.

If desired, we could have only one IntrinsicAttributes interface with both ref and key. This would slightly simplify the .d.ts and implementation at the expense of not solving problem C (it would be legal to write an incorrect ref attribute on a SFC JSX element).

Decision Points

  • What to do about SFC detection when JSX namespace isn't filled in:
    • Check for assignable to Element but not assignable to ElementClass
    • Ignore the problem and default to whatever behavior
    • Something else
  • Props simplification: Choose one:
    • Keep as-is
    • Add IntrinsicAttributes
    • Add IntrinsicAttributes and IntrinsicClassAttributes

/cc @ahejlsberg @billti

Metadata

Metadata

Assignees

Labels

CommittedThe team has roadmapped this issueDomain: JSX/TSXRelates to the JSX parser and emitterFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions