Skip to content

Repository files navigation

react-annotation

The Flow's React Syntax in TypeScript with an intuitive JSDoc-compliant annotation.

Warning

This project is still in the very early stages of development and is not yet ready for use.

Component Annotation

Basic Usage

You can declare a component with Component Annotation by adding a comment block above the component declaration. The comment block should start with @component and can include a @description tag to describe the component.

import*asReactfrom"react";/** * @component */functionIntroduction({ name, age }: {name: string;age: number}){return<h1>My name is {name} and I am {age} years old</h1>;}

Rules for Components

Component Annotation enforces a few restrictions in components to help ensure correctness:

The return values must be a subtype of React.Node, otherwise React may crash while rendering your component.

  1. All branches of a component must end in an explicit return. Even though undefined is a valid return value, we've seen many instances where an explicit return would have
  2. prevented bugs in production.
  3. You cannot use this in a component.

So these components are invalid:

import*asReactfrom"react";/** * @component */functionInvalidReturnValue(){returnnewObject();// ERROR: Value does not match `React.Node` type}/** * @component */functionImplicitReturn(someCond: boolean){if(someCond){return<h1>Hello World!</h1>;}// ERROR: No return in this branch}/** * @component */functionUsesThis(){this.foo=3;// ERROR: Accessing `this`returnnull;}

Hook Annotation

Basic Usage

import{useEffect,useState}from"react";/** * @hook */exportfunctionuseOnlineStatus(initial: boolean): boolean{const[isOnline,setIsOnline]=useState(initial);useEffect(()=>{// ...},[]);returnisOnline;}

Enforcing the Rules of React with Hook Syntax

With hook annotation, we can now unambiguously distinguish syntactically between hooks and non-hooks. @react-annotation/typescript-plugin will use this information to enforce a number of the rules of hooks and Rules of React generally.

Preventing Conflation of Hooks and Functions

The distinction between hooks and regular functions is reflected in the JSDoc annotations. Because of the different properties that hooks and functions must obey, it’s TypeScript error to pass a value defined as a hook into a position that expects a function type, and an error to pass a regular JavaScript function into a position that expects a hook.

import{useEffect,useState}from"react";/** @hook */functionuseMultiplier(x: number): number{const[y,setY]=useState(1);useEffect(()=>{setY(0);});returnx*y;}/** @component */functionMapper({ args }: {args: number[]}){constmultArgs=args.map(useMultiplier);// ^^^^^^^^^^^^^// - ERROR: Expected a function type instead of a React hookreturnmultArgs;}

In addition, Hook Annotation enforces that callees with hook-like names inside hooks and components are indeed hooks. We also ensure that callees inside of regular function definitions are never hooks.

/** @hook */functionuseHook(){returnnull;}functionregularJavascript(){constx=useHook();// ^^^^^^^^^// - ERROR: Cannot call a hook outside of a component or hook}/** @component */functionComponent(){constrenamedHook=useHook;renamedHook();// ^ ERROR: Cannot call a hook outside of a component or hookreturnnull;}

Render Types

A component can declare what it renders using the renders keyword:

import*asReactfrom"react";/** * @component */functionHeader({ size, color }: {size: string;color: string}){return<div/>;}/** * @component * @renders Header */functionLargeHeader({ color }: {color: string}){return<Headersize="large"color={color}/>;// Ok!}

When you declare that your component renders some specific element, you can return any component that eventually renders that component in its renders chain:

import*asReactfrom"react";/** * @component */functionHeader({ size, color }: {size: string;color: string}){return<div/>;}/** * @component * @renders Header */functionLargeHeader({ color }: {color: string}){return<Headersize="large"color={color}/>;}/** * @component * @renders Header */functionLargeBlueHeader(){// You could also use `@renders LargeHeader` abovereturn<LargeHeadercolor="blue"/>;}

Components can specify props that render specific elements:

import*asReactfrom"react";/** * @component */functionHeader({ size, color, message }: {size: string;color: string;message: string}){return<h1style={{ color }}>{message}</h1>;}interfaceLayoutProps{/** * @renders Header */header: React.ReactElement;}/** * @component */functionLayout({ header }: LayoutProps){return(<div>{header}<section>Hi</section></div>);}

And you can pass an element of either Header, or an element of a component that renders Header, to that prop:

<Layoutheader={<LargeBlueHeader/>}/>;

You cannot pass a component that does not render a header to a render type expecting a header:

import*asReactfrom"react";/** * @component */functionFooter(){return<footer/>;}interfaceHeaderProps{size: string;color: string;message: string;}/** * @component * @renders Header */functionHeader({ size, color, message }: HeaderProps){return<h1style={{ color }}>{message}</h1>;}interfaceLayoutProps{/** * @renders Header */header: React.ReactElement;}/** * @component */functionLayout({ header }: LayoutProps){return<div>{header}</div>;}<Layoutheader={<Footer/>}/>;// ^^^^^^^^^^// - ERROR: `Footer` element does not render `Header` in property `header`. [incompatible-type]

Integrating with a design system

Render types are designed to make integrating with a design system simple. If a prop in the design system component expects a render type, you can copy/paste that type onto your component to integrate with the design system:

import*asReactfrom"react";/** * @component */functionHeader(){return<h1>Header!</h1>;}interfaceLayoutProps{/** * @renders Header */header: React.ReactElement;}/** * @component * @renders Header */functionLayout({ header }: LayoutProps){return<div>{header}</div>;}// Copy-paste the header props' type!/** * @component * @renders Header */functionProductHeader(){// We must return a value that renders a Header to satisfy the signaturereturn<Header/>;}// And now you can integrate with the design system!<Layoutheader={<ProductHeader/>}/>;// OK!

Rendering Optional Elements

​ You may want to describe a component that can take a child that may eventually render an element or nothing. You can use a specialized render type variant renders? to achieve this:

import*asReactfrom"react";/** * @component */functionDesignSystemCardFooter(){return<div>Footer Content</div>;}interfaceDesignSystemCardProps{children: React.ReactNode;/** * @renders? DesignSystemCardFooter */footer?: React.ReactElement;}/** * @component */functionDesignSystemCard({ children, footer }: DesignSystemCardProps){return<div>{children}{footer}</div>;}// With these definitions, all of the following work:<DesignSystemCardfooter={<DesignSystemCardFooter/>}>Card</DesignSystemCard>;<DesignSystemCardfooter={null}>Card</DesignSystemCard>;<DesignSystemCardfooter={undefined}>Card</DesignSystemCard>;<DesignSystemCardfooter={false}>Card</DesignSystemCard>;/** * @component * @renders? DesignSystemCardFooter */functionProductFooter(hasFooter?: boolean){returnhasFooter&&<DesignSystemCardFooter/>;}<DesignSystemCardfooter={<ProductFooter/>}>Card</DesignSystemCard>;

Rendering Lists

You may want to describe a component that can take any amount of children that render a specific element as props. You can use a specialized render type variant renders* to achieve this:

import*asReactfrom"react";/** * @component */functionDesignSystemMenuItem(){return<li>Menu Item</li>;}interfaceDesignSystemMenuProps{/** * @renders* DesignSystemMenuItem */children: React.ReactNode;}functionDesignSystemMenu({ children }: DesignSystemMenuProps){return<ul>{children}</ul>;}// With these definitions, all of the following work:constmenu1=(<DesignSystemMenu><DesignSystemMenuItem/></DesignSystemMenu>);constmenu2=(<DesignSystemMenu><DesignSystemMenuItem/><DesignSystemMenuItem/></DesignSystemMenu>);constmenu3=(<DesignSystemMenu>{[<DesignSystemMenuItem/>,<DesignSystemMenuItem/>,]}<DesignSystemMenuItem/></DesignSystemMenu>);/** * @component * @renders* DesignSystemMenuItem */functionProductMenuItem(){return<DesignSystemMenuItem/>;}constmenu4=(<DesignSystemMenu>{[<ProductMenuItem/>,<DesignSystemMenuItem/>,]}<DesignSystemMenuItem/></DesignSystemMenu>);

Disclaimer

This project is not affiliated with Meta Corporation or facebook/react project or team, nor is it endorsed or sponsored by them.

This project is and will continue to maintain that 90% of the code is written by humans.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

(Draft) The Flow's React Syntax in TypeScript with an intuitive JSDoc-compliant annotation.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages