loading...

TypeScript with React Functional Components

wpreble1 profile image Will Preble ・4 min read

TypeScript has become a very popular enhancement for JavaScript applications. TypeScript is a superset of JavaScript that forces static typing and compiles into plain JavaScript. Similar to other statically-typed languages like C# or Java, it forces you to be deliberate with variable types by declaring them upon creation.

In this blog, I’ll cover the basics of incorporating TypeScript into the popular front-end framework (er… library) React with a focus on functional components and hooks.

If you’re completely new to TypeScript, check out my introduction.

Getting Started

To get up and running, let’s create a new React-TypeScript project directory with the popular generator, Create React App. The following command will install the basic type libraries for React, give us a few scripts for testing and building, and give us a default tsconfig file.

npx create-react-app my-app --template typescript

Next, let’s run the start script to see the default React App.

npm run start

This should automatically open up a browser window. A spinning React logo is cool, but let’s get rid of this bloat so that we can focus on our changes.

Replace the div in the return statement with a simple Hello World message.

<div className="App">
  <h1>Hello World</h1>
</div>

If you leave your left your start script running, you should see this change reflected in your browser.

Functional Component with Properties

Create a file called Child.tsx with the following starter code. This will create a simple component with three properties: name, id, and bio. Bio should be set to a default value.

import React from 'react';

const Child = ({ name, id, bio = 'Bio empty' }) => {
  return (
    <div>
      <h2>Name: {name}, Id: {id}</h2>
      <p>{bio}</p>
    </div>
  )
};

export default Child;

Import the Child module into your app component, and add the following element below the h1 tag.

<Child name="Billy" id={123} />

Even this simple rendering will cause compiling errors. This is because we haven’t typed our properties. If you look closely, we haven’t typed our Child component function either. Thankfully, we can make use of the React types library by simply importing the FC type. This is an alias for FunctionalComponent which is also acceptable. We’ll also import ReactElement which will be the return signature of the function

import React, { FC, ReactElement } from 'react';

Next, type the properties for the Child component. In order to use a default parameter for bio, we will make it optional with the ? character.

type ChildProps = {
  name: string,
  id: number,
  bio?: string,
}

Finally, define the function type and the return type, ReactElement.

const Child: FC<ChildProps> = ({/* destructured props */}): ReactElement => { 
  /* function body */ 
};

If you are using an IDE that is compatible with TypeScript such as VSCode, you can hover over the property to confirm the type has been defined correctly.

If you try to change the type of the attribute in the parent component, such as passing a string for an id, TypeScript will throw a compile error.

Your child component should now be rendering correctly.

React Hooks

useState()

When using the useState hook, make sure to set the initial value to the correct type, and TypeScript will use type inference to determine the proper typing. A common pattern would be to establish the initial value as a prop.

const Child: FC<{ initialClick?: number }> = ({ initialClick = 0 }): ReactElement => {
  const [click, setClick] = useState(initialClick);
  return (
    <div>
      <p>Click: {click}</p>
      <button onClick={() => setClick(click + 1)}>Click Me!</button>
    </div>
  )
};

TypeScript can also infer type from a hard-coded initial state.

// click will be inferred to be a number
const [click, setClick] = useState(0);

useEffect()

useEffect, the swiss army hook that replaced component lifecycle methods, accepts two parameters. The first must be a function, and a second optional parameter for specific states. As long as these two parameters are the correct type, TypeScript will compile correctly without any extra code. Here is a simple example to highlight the useEffect hook. This useEffect will only trigger when the component is loaded and when Button A is clicked.

const Child: FC<ChildProps> = (): ReactElement => {
  const [clickA, setClickA] = useState(0);
  const [clickB, setClickB] = useState(0);

  useEffect(() => {
    if (clickA === 0) {
      console.log('Component loaded!')
    } else {
      console.log('Button A was clicked!');
    }
  }, [clickA]);

  return (
    <div>
      <p>A Clicks: {clickA}</p>
      <p>B Clicks: {clickB}</p>
      <button onClick={() => setClickA(clickA + 1)}>Button A</button>
      <button onClick={() => setClickB(clickB + 1)}>Button B</button>
      <p id="click-a"></p>
    </div>
  )
};

useRef()

useRef is an interesting hook because it can give a parent element information about a child element. Here is a typical assignment that can occur inside of the function body of a functional component.

const buttonElement = useRef<HTMLButtonElement>(null);

Assign the constant to the ref attribute of the child component.

<button onClick={() => handleClick()} ref={buttonElement}>Button A</button>

Once you assign a reference, you can use any element property or method on that reference by accessing the .current property. Below, I’m using the getBoundingClientRect method to access position info about the button. However, TypeScript won’t be happy with this code.

const handleClick = () => {
  // this will throw an error because buttonElement.current could be null
  setButtonInfo(buttonElement.current.getBoundingClientRect());
}

A simple workaround is to wrap your reference in a conditional.

const handleClick = () => {
  if (buttonElement.current !== null) {
    setButtonInfo(buttonElement.current.getBoundingClientRect());
  }
}

When putting it all together, make sure to assign the type of ref you are assigning, in this case an HTMLButtonElement. Notice I also assigned the type of the buttonInfo state to a ClientRect.

const Child: FC<{initialInfo?: ClientRect}> = ({ initialInfo }): ReactElement => {
  const [buttonInfo, setButtonInfo] = useState(initialInfo);

  const buttonElement = useRef<HTMLButtonElement>(null);

  const handleClick = () => {
    if (buttonElement.current !== null) {
      setButtonInfo(buttonElement.current.getBoundingClientRect());
    }
  }

  return (
    <div>
      <button onClick={() => handleClick()} ref={buttonElement}>Button A</button>
      <p>Top: {buttonInfo?.height}</p>
      <p>Bottom: {buttonInfo?.width}</p>
    </div>
  )
};

Conclusion

In the beginning, TypeScript can feel like an encumbrance to JavaScript programmers who are not used to thinking much about type. However, these habits can lead to tricky bugs and waste lots of time in development, especially in large or complicated applications. TypeScript forces you to adopt better habits and has great support from the React community.

Hope this blog was helpful. For more information about TypeScript and React including integration with class components, I recommend this blog.

Posted on by:

wpreble1 profile

Will Preble

@wpreble1

Always learning...

Discussion

markdown guide