๐Ÿ”ŒReact + TS

Initialize CRA with TypeScript.

To start a new Create React App project with TypeScript, you can run:

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

# or, using yarn
yarn create react-app my-app --template typescript

In case you want to add typescript to an existing React project, you can add the following dependencies:

# using npm
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

# or, using yarn
yarn add typescript @types/node @types/react @types/react-dom @types/jest

Next, rename any file to be a TypeScript file (e.g. src/index.js to src/index.tsx) and restart your development server!

Type errors will show up in the same console as the build one. You'll have to fix these type errors before you continue development or build your project. For advanced configuration, see here.

Components

Working with components in React using TypeScript.

Props

There is a special way of defining components in a .tsx file, to use the props in the component. We could also define the prop type definition in the functional component parameters, but that would be cumbersome for various components that our app might have. So there is an alternative provided by '@types/react', which is available via the 'react' library, and we use the 'FC' component from that library.

components/Todos.tsx
import {FC} from 'react';

const Todos: FC = (props) => {
  return (
    <>
      <div>
        {props.children}
      </div>
    </>
  );
}

export default Todos;

This code snippet above shows how we import and use the FC component function. In the end, it is a type definition for a function, which stands for 'Functional Component', and internally defines type definitions for props and children for a functional component.

Type Annotations

Here, React.FC or FC is a generic type, which is defined by React on a functional component, and we can now have type definitions for our props in this component. We are not creating a new generic type, but are plugging in a concrete value for that internally used generic type before that type T defined by that React.FC type. We are doing this so that typescript does not infer the generic type here because here we are not calling some generic function with some parameters where the values could then be used to for the inference, but rather we're defining a function and letting typescript know how it should treat this function internally that it should get some props defined by us and merge those with some base props like the children prop, which all functional components have.

/* Use a generic type and explicitly set the concrete type
 * that should be used for the usage of this generic FC type.

 * Angular brackets are to tell React that we are using the generic
 * type FC, but with concrete values that is our own 'props' object,
 * where we describe our own props for this specifc functional component.

 * It is generic, because different functional components will have
 * different prop definitions.
 */
 
 import {FC} from 'react';
 
 const Todos: FC<{items: string[]}> = (props) => {
   // FC<{...}> merges our own prop definition to existing default prop
   // definition defined in React.FC
   //return...
 }
 
 export default Todos;

We use this, for better auto complete and more descriptive code in our editor, but it also has some other advantages, the main one being that the place where we use this <Todos> component. If we do not pass the "items" prop to the <Todos> component, Typescript will show a warning that we are missing props that we have defined in our functional component.

Data Models

Since the props we pass to our functional component will not always be simple, they will carry more info with them , typically in an object, it is better and easier to read the code that is modular, and uses type, or interface or class models (type definitions) to describe the props for our React.FC type annotation that we add to the existing props. We can define these using interface as well, but here we will look at class models.

models/todo.ts
class Todo {
  id: number;
  text: string;
  
  constructor(todoText) {
    this.text = todoText;
    this.id = Math.random();
  }
}

export default Todo;

This is the type of object that we will pass to our <Todos> component, where the prop 'items' will have an array of type 'Todo', as defined in our model file todo.ts. In our App component, where we use our Todos component, we need to pass the 'items' prop with the data so that it can be rendered dynamically in our <Todos> component. The way we create a new data in the items prop is a little different, i.e, by calling the constructor of the class model we defined in todo.ts file.

components/ Todos.tsx
import {FC} from 'react';
import Todo from '../models/todo';

const Todos: FC<{items: Todo[]}> = () => {
  return(
    <ul>
      {props.items.map(items => <li key={item.id}>{item.text}</li>)}
    </ul>
  );
}

export default Todos;

This makes our code well structured and difficult to misuse our component in any way. This ensures data cannot be manipulated, and that we get most of the errors during development instead of runtime. This is the benefit of using Typescript with React!

Practice

Let's outsource the <li> element from the above example into it's own component <TodoItem>.

components/TodoItem.tsx
import {FC} from 'react';

const TodoItem: FC<{text: string}> = (props) => {
  return <li>{props.text}</li>
}

export default TodoItem;

Forms

onSubmit event

While submitting forms in React, we use the event.preventDefault() to prevent the browser default action when we submit a form. While handling the form submission, we need to provide the "event" parameter to the submit handler, in order to prevent the browser default action. Typescript does not know the type of the "event" that we pass to this submit handler function, and thus we use a special type, a type annotation provided by React, called "FormEvent", which will tell typescript that this is an event object type which comes from the browser automatically when listening to a onSubmit event on a form. There are other events as well, like "MouseEvent", which we could get on a onClick event.

useRef

To get the input values of what the user has entered, we can either listen to every keystroke, like the onChange event, but here we will prefer to use the useRef, to point to an input and get the input value at once. We need to be explicitly about the type of data we will be storing in this ref because will show an error since we do not provide any type to the ref we create using the useRef() hook. To provide a type to the newly created ref, we can use generics.

useRef() is a generic type out of the box in our project. We can add the angled brackets and add a concrete type of ref we want to create in this instance. Now, the concrete value or the HTML element that we want to connect the ref with, is defined within these angled brackets.

Here, we want to use the ref with an <input> HTML element, so the concrete type for this element is a built-in type, called HTMLInputElement. All the DOM elements have a built-in type, which we can use to refer to them. For example, a <button> will have HTMLButtonElement, a <p> will have HTMLParagraphElement, et al.

So now we are making clear that the ref we are actually creating here, will actually be connected to an HTMLInputElement. But we also need to provide a starting value to it, since this ref could be assigned to another HTML element. Since we have no initial connection, we can make this as null.

Fetching the ref value

To get the value of the reference, we can use it in the same way we used to get the value in JSX files, but the autocomplete suggestion from our IDE adds a '?' after 'current', like so:

inputRef.current?.value. This question mark added here signals to typescript that it tries to access 'value' and if it succeeds, the value will be stored in the assigned variable, and if that fails, i.e, the connection between the HTML element and the ref is not established, it will store null in the assigned variable.

components/NewTodoItem.tsx
import {useRef, FormEvent} from 'react';

const NewTodoItem = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const submitFormHandler = event: FormEvent => {
    event.preventDefault();
    const enteredVal = inputRef.current?.value;
    // The inferred type for enteredVal will be string | undefined,
    // if we hover over it in our IDE.
  }
  
  return (
    <form onSubmit={submitFormHandler}>
      <label htmlFor="todo">Enter a Todo</label>
      <input id="todo" type="text" ref={inputRef}/>
      <button>Add Todo</button>
    </form>
  );
}

In case we are sure that the connection has been established between the ref and the HTML element, we can then get the value of the ref by using the same syntax mentioned above, but just replacing the '?' with a '!' (exclamation). This tells typescript, that we are sure the ref will have a value that is not null / undefined. With that, if we hover over the variable where we store this value, the inferred type will be a string, and nothing else.

const enteredVal = inputRef.current!.value;
// Inferred type for enteredVal will be string, if we hover
// over it in our IDE.

The question mark (?) and exclamation (!) operators are not specific to React, but they are used in typescript for checking if a value is null / undefined, or has a definite value other than null / undefined.

Function Props

Moving on with the same example as above, we want to update the Todos list with the new Todo added by the user via the NewTodoItem component. But first, we need a function, that will send this enteredVal to the place we are using the NewTodoItem component, i.e, the App component. We do this in the same way we used to before, by sending a function as a prop to NewTodoItem component, which adds the enteredVal to the list of "todos" managed in the App component. We will have to define the prop type in the NewTodoItem component, as we did before.

components/NewTodoItem.tsx
import {useRef, FormEvent, FC} from 'react';

const NewTodoItem: FC<{onAddTodo: (text: string) => void}> = (props) => {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const submitFormHandler = (event: FormEvent) => {
    event.preventDefault();
    const enteredVal = inputRef.current!.value;
    props.onAddTodo(enteredVal);
  }
  
  return(
    <form>
     //...
    </form>
  )
}

export default NewTodoItem;

State

We want to tell typescript that the type of data managed by the useState() hook in our case would be an array of Todo types, that we defined earlier in the models/ folder, inside of the todo.ts file. We can do this, because the useState() is a generic type again, and we can give it a concrete type value. We would still need to give the default value to useState() hook when we define it, and store the returned values in a de-structured array, just like before.

App.tsx
import {useState} from 'react';

import NewTodoItem from './components/NewTodoItem';
import Todos from './components/Todos';

import Todo from './models/todo';

const App = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  
  const addTodoHandler = (text: string) => {
    const newTodo = new Todo(text);
    setTodos(prevTodos => prevTodos.concat(newTodo));
  } ;
  
  return(
    <>
      <NewTodoItem onAddTodo={addTodoHandler}/>
      <Todos items={todos}/>
    </>
  );
};

export default App;

Removing a Todo Item

We can remove a Todo item from the todo list by using the following code snippets, which in the end do not need to use the MouseEvent from the React library. The problem we will face here, is that we will create a prop chain, by sending the delete function prop from <TodoItem> to <Todos> and then finally <App>, where we manage the state of all Todo items.

components/TodoItem.tsx
import {FC} from 'react';

const TodoItem: FC<{
  text: string;
  onRemoveTodo: () => void;
}> = (props) => {
  return <li onClick={props.onRemoveTodo}>{props.text}</li>
}

export default TodoItem;

Context API

We are creating prop chains when passing functions as props for adding and remove items from the todo list. So, the basic solution to avoid prop chains is to use the React Context API.

We will start off by creating a new file called 'todos-context.tsx' inside a new folder, called store/, which will contain our context object, as well as the context provider for our <App>

store/todos-context.tsx
import {createContext, useState, FC} from 'react';
import Todo from '../models/todo';

type TodosContextTypeObj = {
  items: Todo[];
  addTodo: (text: string) => void;
  removeTodo: (id: number) => void;
}

export const TodosContext = createContext<TodosContextTypeObj>({
  items: [],
  addTodo: () => {},
  removeTodo: (id: number),
});

export const TodosContextProvider: FC = (props) => {
  const [todos, setTodos] = useState<Todo[]>([]);
  
  const addTodoHandler = (text: string) => {
    const newTodo = new Todo(text);
    setTodos(prevTodos => prevTodos.concat(newTodo));
  }
  
  const removeTodoHandler = (id: number) => {
    setTodos(prevTodos => prevTodos.map(item => item.id !== id));
  }
  
  const contextValue: TodosContextTypeObj = {
    items: todos,
    addTodo: addTodoHandler,
    removeTodo: removeTodoHandler,
  }
  
  return (
    <TodosContext.Provider value={contextValue}>
      {props.children}
    </TodosContext.Provider>
  );
}

Some important concepts from the above code:

  • We use the type alias TodosContextTypeObj, to define the structure of the context object, which we will be using across our app. We do this for better linting, as our IDE will point out to the places where we misuse the context object types, or declare them incorrectly, or in cases where we add functions to the context, but forget ot give them the appropriate parameters.

  • The same type alias is used to define the structure of the context value, which we provide to the context provider.

  • In case there is any error, in defining or using the context object values, the IDE will point out the specific place where we have misused the context object property.

With the help of context API, we remove prop chaining from the app, and thus we can then remove the prop type definitions from the components, and use the context object to then use all the props from within this context.

src/App.tsx
import Todos from './components/Todos';
import NewTodoItem from './components/NewTodoItem';

import {TodosContextProvider} from './store/todos-context';

const App = () => {
  return (
    <TodosContextProvider>
      <NewTodoItem />
      <Todos />
    </TodosContextProvider
  );
}

export default App;

The final project can be found here. The same project is also available with Redux-Toolkit here.

Last updated