DEV Community

Matti Bar-Zeev
Matti Bar-Zeev

Posted on • Edited on

Converting a React component to TypeScript

In this post you will join me as I modify a simple component to start utilizing TypeScript.
The WordSearch game which I’m experimenting on was built using CreateReactApp so I will follow their guide on how to enable TS on an existing project.

First we need to install the packages which enable typescript on a project

  • Typescript - the package which enables the actual TS compiler
  • @types/node - the package which contains type definitions for Nodejs
  • @types/react - the package which contains type definitions for React
  • @types/react-dom - the package which contains type definitions for React DOM
  • @types/jest - the package which contains type definitions for Jest

The docs from CreateReactApp tell me to install these as runtime deps, but I think that their place is under the dev deps, so this is where I will install them :)

I’m going to take the AddWord component and convert it to use TS. This component is responsible for adding a new word to the in the words panel for the WordSearch game.

image

Here is the original code which will help you follow through:

import React, {Fragment, useEffect, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';

const AddWord = ({onWordAdd}) => {
   const inputEl = useRef(null);
   const [newWord, setNewWord] = useState('');
   const [disable, setDisable] = useState(true);

   useEffect(() => {
       // A word is valid if it has more than a single char and has no spaces
       const isInvalidWord = newWord.length < 2 || /\s/.test(newWord);
       setDisable(isInvalidWord);
   }, [newWord]);

   function onAddClicked() {
       onWordAdd && onWordAdd(inputEl.current.value);
       setNewWord('');
   }

   function onChange(e) {
       const value = e.target.value;
       setNewWord(value);
   }

   return (
       <Fragment>
           <input
               type="text"
               name="new"
               required
               pattern="[Bb]anana|[Cc]herry"
               ref={inputEl}
               placeholder="Add word..."
               value={newWord}
               onChange={onChange}
           />
           <button onClick={onAddClicked} disabled={disable}>
               <Add></Add>
           </button>
       </Fragment>
   );
};

export default AddWord;
Enter fullscreen mode Exit fullscreen mode

I start by changing the file extension to .tsx - src/components/AddWord.js > src/components/AddWord.tsx

Launching the app I’m getting my first type error:

TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(4,19):
Binding element 'onWordAdd' implicitly has an 'any' type.  TS7031

   2 | import Add from '@material-ui/icons/Add';
   3 |
 > 4 | const AddWord = ({onWordAdd}) => {
     |                   ^
Enter fullscreen mode Exit fullscreen mode

Let’s fix that.
The problem here is that the component does not declare the type of props it allows to be received. I saw 2 methods of addressing this issue. One is using the React.FC and the other is approaching this function component as a function and therefore regard its typing as a function without React’s dedicated typings. Reading Kent C. Dodds' article about the issue, and also the caveats of using React.FC in this detailed StackOverflow answer, I decided to go with the conventional function typing way.
Ok, so we need to define the props type. I would like to go with Interface instead of a type, coming from an OOP background, I know that working against interfaces is by far much more flexible.
There is a single prop this component receives and it is a callback function, which has a string argument and returns nothing (I like to mark my interfaces with an “I” prefix).
Our props interface looks like this:

interface IAddWordProps {
   onWordAdd: (value: string) => void;
}
Enter fullscreen mode Exit fullscreen mode

And the usage looks like this:

const AddWord = ({onWordAdd}: IAddWordProps) => {
...
Enter fullscreen mode Exit fullscreen mode

That solved that, on to the next error:

TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(20,32):
Object is possibly 'null'.  TS2531

   18 |
   19 |     function onAddClicked() {
 > 20 |         onWordAdd && onWordAdd(inputEl.current.value);
      |    
Enter fullscreen mode Exit fullscreen mode

Which is true, the inputEl can potentially be null, so how do we go about it?
In general, I don't like suppressing errors and warnings. If you decide to use a tool you don’t need to be easy on the “disable rule” configuration of it, so let’s try and really solve this one.
First I would like to set a type to the inputEl ref, and it can be either null or a React.RefObject interface which has a generics type to it. Since we’re dealing with an input element, it would be HTMLInputElement. The inputEl typing looks like this now:

const inputEl: RefObject<HTMLInputElement> | null = useRef(null);
Enter fullscreen mode Exit fullscreen mode

Still, this does not solve our main issue. Let’s continue.
One option to solve this issue is using optional-chaining, which means that we know and prepare our code to gracefully handle null pointers. The handler looks like this now:

function onAddClicked() {
   onWordAdd && onWordAdd(inputEl?.current?.value);
Enter fullscreen mode Exit fullscreen mode

But once we do that we have broken the interface of the props we defined earlier, since it expects to receive a string and now it can also receive undefined, so let’s fix the interface to support that as well:

interface IAddWordProps {
    onWordAdd: (value: string | undefined) => void;
}
Enter fullscreen mode Exit fullscreen mode

Done. On to the next error.

TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(24,23):
Parameter 'e' implicitly has an 'any' type.  TS7006

   22 |     }
   23 |
 > 24 |     function onChange(e) {
      |                       ^
   25 |         const value = e.target.value;
Enter fullscreen mode Exit fullscreen mode

The solution here is simple - I’m adding the ChangeEvent type to e. Now it looks like this:

function onChange(e: ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setNewWord(value);
}
Enter fullscreen mode Exit fullscreen mode

This is not a “React type” and as for now I don’t see any reason to use React types when not needed (if you do know of such a reason, please share in the comments).

And that’s it! The application is back and running :)
Below you can find the modified code (with some additional, non-critical types added) and you can compare it to the original one at the start of this post.

update -
After some great feedback in the comments below (and on Reddit) I've made some modifications in the code accordingly. Thanks guys.

import React, {ChangeEventHandler, MouseEventHandler, RefObject, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';

interface IAddWordProps {
    onWordAdd?: (value: string | undefined) => void;
}

const AddWord = ({onWordAdd}: IAddWordProps) => {
    const inputEl: RefObject<HTMLInputElement> | null = useRef(null);
    const [newWord, setNewWord] = useState('');
    const [disable, setDisable] = useState(true);

    const onAddClicked: MouseEventHandler<HTMLButtonElement> = () => {
        onWordAdd?.(newWord);
        setNewWord('');
    };

    const onChange: ChangeEventHandler<HTMLInputElement> = ({currentTarget: {value}}) => {
        setNewWord(value);
        // A word is valid if it has more than a single char and has no spaces
        const isInvalidWord: boolean = value.length < 2 || /\s/.test(value);
        setDisable(isInvalidWord);
    };

    return (
        <>
            <input
                type="text"
                name="new"
                required
                pattern="[Bb]anana|[Cc]herry"
                ref={inputEl}
                placeholder="Add word..."
                value={newWord}
                onChange={onChange}
            />
            <button onClick={onAddClicked} disabled={disable}>
                <Add />
            </button>
        </>
    );
};

export default AddWord;
Enter fullscreen mode Exit fullscreen mode

Cheers :)

Hey! If you liked what you've just read be sure to also visit me on twitter :) Follow @mattibarzeev 🍻

Top comments (7)

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Great code review 👍 thanks!
This this "sandbox" game is being tweaked by me as time goes by I tend to focus on the specific reasons for modifying it, and so some original code can surely use some modern react practices :)

Collapse
 
jbartusiak profile image
Jakub Bartusiak • Edited

Good walkthrough. The point of using react events is to guarantee that they get handled the same way, no matter which browser the app is running it - but agreed that for the demo here that's not really crucial. While you're typing your event to a change event, under the hood you're still using the synthetic event. The interface is the same, that's why you don't see any errors.

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Thanks for the informative feedback! I am still not quite sure, bit something tells me that if we can avoid additional abstractions, we should. So in general, if the "plain" interface is applicable, I don't a reason to use the abstraction.

Collapse
 
constantiner profile image
Konstantin Kovalev

I'd suggest not to use FC< IAddWordProps > (or FunctionComponent<IAddWordProps>) here because of this type supposes component also accepts children (in this case not). Use VoidFunctionComponent<IAddWordProps> (or VFC<IAddWordProps>) instead.

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

BTW, I'm not using FC on purpose ;)

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

... and another thing - I like to keep my component's state as such, even though the "disabled" can be calculated on render. I find it more convenient if later on I would like to introduce a prop which initialize that state and it is much more readable IMO.

 
constantiner profile image
Konstantin Kovalev

But for this particular component even if you pass some children, they will attach nowhere. So passing them is useless. Why not restrict them then? Or I missed the point?