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

Proposal: Component Refs and Typescript strict typing #75

Open
TheNando opened this issue Jun 14, 2019 · 0 comments
Open

Proposal: Component Refs and Typescript strict typing #75

TheNando opened this issue Jun 14, 2019 · 0 comments
Labels
💡 IDEA enhancement New feature or request

Comments

@TheNando
Copy link

I'd like to propose the following pattern for Typescript types (which may be better suited going into the Helix repo, directly) and component references (which is most appropriate in this project)

Objective

A React component may contain many Helix web components within. It will often be necessary to access the methods and properties of those web components to manage their state from within a stateful React component (it's not possible for stateless components)

Defining strong types

In order to use Web Components in React with Typescript, they must be defined as JSX intrinsic elements. This means then need types to define all the properties to register with JSX. However, JSX only knows about the attributes, while when used within the component, the user may also need to access the methods and/or properties. This means two types will be needed (because JSX will complain, for example, if a web component has a method defined, but it's not passed in as an attribute when used)

Example Web Component Type

// HXTabset is the intrinsic type for JSX, only including attributes
// WebComponent is a custom type defining HTML Element attributes, like 'class'
interface HXTabset extends WebComponent {
  // All attributes go here in kebab case
  'current-tab'?: number;
}

// HXTabsetElement is for using the web component instance within
// React Component lifecycle hooks and instance methods
interface HXTabsetElement extends HTMLElement {
  // All methods and properties go here
  currentTab: number;
  selectNext: Function;
}

Accessing web component instance

In order for a React component to access the instance of a child web component, it must have access to a reference. JSX recognizes a universal ref property that allows for obtaining that.

class MyReactComponent extends React.Component<MyProps> {
  render() {
    return (<hx-tabset ref={ /* access the ref here */} />)
  }
}

There are a number of options to obtain the ref: a string literal which will be added to this.refs as a generic React.ReactInstance type, via a callback, or using React.createRef. The createRef method offers the best solution, as it is a generic which will allow us to type it in one place instead of anywhere we use the reference. We can add the ref as a class field and pass the field reference to the ref property to assign it.

class MyReactComponent extends React.Component<MyProps> {
  // Use the Element type to access the methods and properties
  private tabsetRef = React.createRef<HXTabsetElement>();

  render() {
    // Assign the element web component instance to the empty React component instance 
    return (<hx-tabset ref={this.tabsetRef} />)
  }
}

This reference will contain a current member which yields access to the web component instance directly. It's worth note that this reference will initially be null until the React component mounts (which causes the first render, which assigns the reference), so anytime the web component is referenced, Typescript will insist that it may be null. This means in order to use a method on the web component instance, it must either be gated by a null check or non-null assertion, cast to the web component type directly, or defaulted on each use. This can be cumbersome, so a shortcut is recommended.

class MyReactComponent extends React.Component<MyProps> {
  private tabsetRef = React.createRef<HXTabsetElement>();

  // Create a getter to avoid cumbersome boilerplate
  get tabset() {
    // Use a non-null assertion (!) so TS doesn't complain
    return this.tabsetRef.current!;
  }

  componentDidMount() {
    // Now `tabsetRef` is on the instance of the React component, properly typed, and any
    // of the web component instance methods and properties can be used
    this.tabset.addEventListener(...);
    this.tabset.selectNext();
  }
}

One final note: ideally, non-null assertion usage should be avoided. However, this is the cleanest implementation. It is assured that none of the instance methods can be executed until the component has mounted at which point the reference will definitely exist.

@100stacks 100stacks added 💡 IDEA enhancement New feature or request labels Oct 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💡 IDEA enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants