loading...

Notes on TypeScript: Render Props

busypeoples profile image A. Sharif Updated on ・4 min read

Introduction

These notes should help in better understanding TypeScript and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

More Notes on TypeScript

Notes on TypeScript: Pick, Exclude and Higher Order Components

Render Props

Render Props is a popular pattern for enhancing a React Component with additional functionality. It can be interchanged with a higher order component, and choosing the render props pattern or a higher order component is a matter of flavour and depends on the specific use case.

To get a better understanding of the topic, let's build a component that uses a render prop. In the previous "Notes on TypeScript" we built a component that provided an Input component with onChange and value properties.
We can rewrite this higher order component to a render prop implementation.

class OnChange extends React.Component {
  state = {
    value: this.props.initialValue
  };
  onChange = event => {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    this.setState({ value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}

Using the refactored OnChange inside your React application:

<OnChange
  initialValue="hello"
  render={onChangeProps => <Input {...props} {...onChangeProps} />}
/>

We can reuse most of the previously defined types.

type InputProps = {
  name: string,
  type: string
};

type OnChangeProps = {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
  value: string
};

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  render: (onChangeProps: onChangeProps) => JSX.Element
};

type OnChangeState = {
  value: string
};

Our Input component has not changed, we can also reuse that component for this example.

const Input = ({ value, onChange, type, name }: InputProps & OnChangeProps) => (
  <input type={type} name={name} value={value} onChange={onChange} />
);

So now that we have everything in place, let's see how OnChange would be typed.
Interestingly, there's not very much we need to do, to type the onChange component.

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}

Compared to the higher order component implementation, we only need to define props and state for OnChange, in this case using the already existing ExpandedOnChangeProps and OnChangeState and defining the class component as follows:

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState>.

We might want to reuse the functionality in multiple places inside our application. By defining a new component, f.e. ControlledInput, we can combine our OnChange and Input and let developers define the initialValue as well as name and type.

type ControlledInputProps = InputProps & { initialValue: string };

const ControlledInput = ({ initialValue, ...props }: ControlledInputProps) => (
  <OnChange
    initialValue={initialValue}
    render={onChangeProps => <Input {...props} {...onChangeProps} />}
  />
);

Now ControlledInput can be used inside another component and TypeScript will complain when either name, type or initialValue is missing.

<ControlledInput initialValue="testdrive" type="text" name="test" />

Advanced

We might want to enable to either pass the render callback via render or children prop. This requires us to make some changes to our OnChange component. If we recall, our ExpandedOnChangeProps has the following shape:

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  render: (onChangeProps: onChangeProps) => JSX.Element
};

One way to enable passing callbacks as children prop is to change the definition to the following:

type ExpandedOnChangeProps = {
  initialValue: string,
  render?: (onChangeProps: onChangeProps) => JSX.Element,
  children?: (onChangeProps: onChangeProps) => JSX.Element
};

But the above definition has problems, as both or none of the variants could be provided now. What we actually want is to ensure that one of these properties is defined, which is possible defining an explicit RenderProp type:

type RenderProp =
  | { render: (onChangeProps: OnChangeProps) => JSX.Element }
  | { children: (onChangeProps: OnChangeProps) => JSX.Element };

Which means we can rewrite our ExpandedOnChangeProps definition to:

type ExpandedOnChangeProps = {
  initialValue: string
} & RenderProp;

Finally we need to update the render function to handle both possible cases:

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    if ("render" in this.props) {
      return this.props.render({
        value: this.state.value,
        onChange: this.onChange
      });
    }

    if ("children" in this.props) {
      return this.props.children({
        value: this.state.value,
        onChange: this.onChange
      });
    }

    throw new Error("A children or render prop has to be defined");
  }
}

By using "render" in this.props, we can check if render is defined else check if a children property is defined. In case neither properties are defined we throw an error.
Our previously defined ControlledInput could be rewritten to:

const ControlledInput = ({
  initialValue,
  ...props
}: InputProps & { initialValue: string }) => (
  <OnChange initialValue={initialValue}>
    {onChangeProps => <Input {...props} {...onChangeProps} />}
  </OnChange>
);

We should have a basic understanding of how render props can be typed with TypeScript now.

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Posted on by:

busypeoples profile

A. Sharif

@busypeoples

Focusing on quality. Software Development. Product Management. https://twitter.com/sharifsbeat

Discussion

pic
Editor guide
 

Thanks for the article A. Sharif.

It seems like this is a part of a series.
Would you be able to mark series posts in the meta data so it's easy to navigate?

series

 

Thanks for the info Sung Kim!