Skip to content
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

Design preview: Stateless Functional Components in JSX #5478

Closed
RyanCavanaugh opened this issue Oct 30, 2015 · 16 comments
Closed

Design preview: Stateless Functional Components in JSX #5478

RyanCavanaugh opened this issue Oct 30, 2015 · 16 comments
Assignees
Labels
Committed The team has roadmapped this issue Domain: JSX/TSX Relates to the JSX parser and emitter Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh added the Discussion Issues which may not have code impact label Oct 30, 2015
@RyanCavanaugh
Copy link
Member Author

Confirmed hypothesis -- the Delve team hit Problem B

@basarat
Copy link
Contributor

basarat commented Nov 5, 2015

IntrinsicAttributes and IntrinsicClassAttributes

👍

@RyanCavanaugh
Copy link
Member Author

Update - realized that we'd need to define IntrinsicClassAttributes as generic because the ref callback needs the class type for the parameter

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Committed The team has roadmapped this issue and removed Discussion Issues which may not have code impact labels Nov 6, 2015
@RyanCavanaugh RyanCavanaugh self-assigned this Nov 6, 2015
@RyanCavanaugh
Copy link
Member Author

✔️ to implement the following:

  • 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 [non-crashing] behavior
    • Something else
  • Props simplification: Choose one:
    • Keep as-is
    • Add IntrinsicAttributes
    • Add IntrinsicAttributes and IntrinsicClassAttributes<T>
@RyanCavanaugh
Copy link
Member Author

Because even I have a hard time keeping this all straight:


Sample

interface GreeterProps {
    name: string;
}

class Greeter extends React.Component<GreeterProps, {}> {
    render() {
        return <div>Hello, {this.props.name}</div>;
    }   
}

let SimpleGreeter = ({name = 'world'}) => <div>Hello, {name}</div>;

Glossary

  • element type: The class or factory function backing a non-intrinsic element. Example: typeof Greeter
  • element instance type: The type of an instance or invocation of a class or factory function (respectively). Example: Greeter
  • intrinsic element: A built-in element. Intrinsic elements do not have an element type or element instance type (they resolve directly to their element attributes type. Example: div.
  • element attributes type: The type defining the element-specific allowed and required attributes on a JSX element. Example: GreeterProps
  • intrinsic attributes: A set of attributes that are available on all elements. Example: key. Defined by the type JSX.IntrinsicAttributes.
  • intrinsic class attributes: A set of attributes that are available on all class-based elements. Example: ref. Defined by the type JSX.IntrinsicClassAttributes<T>, where T is instantiated to the element instance type.
  • stateless functional component: A function of at most three parameters that returns a JSX.Element. Example: SimpleGreeter
  • augmented attributes type: An intersection of the element attributes type and other types, depending on the type of the element. This is the ultimate type used to determine valid and required attributes on a JSX element expression

Table

Element Type Element Instance Type Element Attributes Type Augmented Attributes Type
Class (Greeter) The class instance, e.g. new Greeter() GreeterProps GreeterProps & JSX.IntrinsicAttributes & JSX.IntrinsicClassAttributes<Greeter>
Intrinsic (div) N/A React.HTMLAttributes (as determined by the type of JSX.IntrinsicElements#div in react.d.ts) React.HTMLAttributes
Stateless Functional (SimpleGreeter) N/A `(the type of the first argument to the function) & JSX.IntrinsicAttributes
dokidokivisual added a commit to karen-irc/karen that referenced this issue Dec 2, 2015
Enable Stateless Function Components infrastructure which TypeScript supports officially

the design details in TypeScript: microsoft/TypeScript#5478

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/karen-irc/karen/512)
<!-- Reviewable:end -->
dokidokivisual added a commit to karen-irc/karen that referenced this issue Dec 2, 2015
Enable Stateless Function Components infrastructure which TypeScript supports officially

the design details in TypeScript: microsoft/TypeScript#5478

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/karen-irc/karen/512)
<!-- Reviewable:end -->
@RyanCavanaugh
Copy link
Member Author

This work has been done

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 1.8 milestone Jan 4, 2016
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Jan 4, 2016
@DanielRosenwasser DanielRosenwasser added the Domain: JSX/TSX Relates to the JSX parser and emitter label Jan 30, 2016
@stepancar
Copy link

@RyanCavanaugh as i understand, now we can use

class Child extends React.Component<{},{}>{
      doSomething(){
      }
      render(){
           return <div></div>
      }

}
class Parent extends React.Component<{},{}>{
      componentDidMount(){
            this.refs.myChild.doSomething();
      }
      render(){
           return (
                 <div>
                       <Child  ref={'myChild'} />
                 </div>
           )
      }
}

right?

@RyanCavanaugh
Copy link
Member Author

@stepancar I think you'll need to declare a refs property in your class if you want full typechecking, though it sounds like React is deprecating string-based ref keys anyway in favor of callback-based refs.

@stepancar
Copy link

@RyanCavanaugh, 1.8 changes
'The ref and key properties will appear with correct types on all components'
I think it will be nice if typescript will provide full type checking for this.refs object by analys render method markup.

@stepancar
Copy link

i think string-based ref is not bad, callback-based ref is more verbose

@stepancar
Copy link

@basarat , thank you!

@dsifford
Copy link
Contributor

Hi there,

I apologize for my ignorance, but I just cannot seem to figure out how this works....

As an illustration, lets say I want to make an Example functional component that accepts all div props plus an additional exampleprop prop of type string. How would I go about doing this? I'm able to achieve it by extending React.Props but depreciation warning is concerning.

I've tried pretty much every flavor of the following:

interface ExampleProps extends JSX.IntrinsicClassAttributes<div> {
  exampleprop: string
}
export const Example = (props: ExampleProps) => 
// See below note for another unrelated issue
<div data-example={props.exampleprop} {...props} />




// Then later on when I render it...

<Example exampleprop='hello'>
  This is my example component with the children prop still rendering appropriately
</Example>

Regarding the note comment above, is the {...props} spread attribute getting fixed (due to the recent React depreciation) or should I be using some other way to spread the other props? It still works fine, but I'm getting bombarded with console warnings.

Any help is greatly appreciated! Thanks in advance 😄

@DanielRosenwasser
Copy link
Member

@dsifford you can avoid the warnings by using our nightlies npm install -g typescript@next where the fix is in, and as far as I know the plan is to port the fix back to TypeScript 1.8 and release 1.8.10.

As for how to go about your scenario, your current approach seems fine, though if you really don't feel like declaring a type for that, you could use an intersection type:

export const Example = (props: JSX.IntrinsicClassAttributes<div> & { exampleprop: string }) => 
    <div data-example={props.exampleprop} {...props} />

Personally, I'm a fan of declaring these prop types explicitly as interfaces because it's a little more readable.

@dsifford
Copy link
Contributor

Thanks (again!) for the clarification @DanielRosenwasser

I'm getting this error when I try to declare the prop types like in my above example:

image

I figured I was just doing it incorrectly. Any ideas?

@DanielRosenwasser
Copy link
Member

Sorry, you probably wanted React.HTMLProps<HTMLDivElement>.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Domain: JSX/TSX Relates to the JSX parser and emitter Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
6 participants