TypeScript Section 14 – Using React with TypeScript


TypeScript

Updated Mar 14th, 2022

Table of Contents

Chapter Details

Module Introduction

React is a popular 3rd-Party for building amazing user interfaces.

Setting Up a React + TypeScript Project

We will build a simple “to-do” app.

Need a project setup for handling JSX and TS. Can be tough on our own. Create-React-App supports TS out of the box. See the React docs.

npx create-react-app . --typescript

We are using a period in the code above, instead of “my-app” so it doesn’t create a new subfolder as we are already in the desired directory.

Can see a pre-existing “tsconfig.ts” file and in the “src” folder we see files with the “.tsx” file extension.

Get started by cleaning up this boilerplate project.

How Do React + TypeScript Work Together?

In the “Apps.tsx” file we see a strange type “React.FC” added to the “App” component and this is provide by the “@react-types” package that is automatically installed.

const TodoList: React.FC = () => {

}

There is a longhand version of this as well but we rarely use.

There is a similar type for class-based React components

Working with Props and Types for Props

Create our first custom component, “TodoList.tsx” file and integrate into the “App.tsx” component.

Want to add a separate “NewTodos” with separate component so we will manage the “todos” in the APp.tsx file and then pass them as props to the “TodoList” component.

But we will get a compilation error while trying to pass props because when using TS with React you need to add types to your props. If you rely on props, you must TS how those props are structured. Add right after the React.FC code in angle brackets or define an interface (optional but often cleaner).

// in app.ts passing props

const App: React.FC = () => {
  const todos = [{id: 't1', text: 'Finish the course'}]
  return (
    <div className="App">
      <NewTodo />
      <TodoList items={todos}/>
    </div>
  )
}
// in TodoList.tsx

interface TodoListProps {
  items: {id: string, text: string}
}

const ToDoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

This helps with “type safety” and gives us auto completion.

Getting User Input with “refs”

Add a second component in a “NewTodo.tsx” file and add into the “App.tsx” file.

In “NewTodo.tsx” file, add a form with an input and a button. Create a new ” todoSubmitHandler ” function and bind to an “onSubmit” on the form.

const NewTodo: React.FC = () => {
  return (
    <form onSubmit={todoSubmitHandler}>
      <div>
        <label htmlFor="todo-text">Todo Text</label>
        <input type="text" id="todo-text"/>
      </div>
      <button type="submit">ADD TODO</button>
    </form>
  )
}

export default NewTodo

When creating the function, passing it the event parameter, and preventing the default form submit, we need to tell TS what type of event to expect. Also create a “ref” to extract what the user entered into the input.

const todoSubmitHandler = (event: React.FormEvent) => {
  event.preventDefault()
  const enteredText = textInputRef.current!.value
}

TS needs to know what “type” of data will be stored in the “useRef”

const textInputRef = useRef<HTMLInputElement>(null)

Since “useRef” works with “.current.value” we need to handle for the “.value” being null by telling TS the “value” will be set and we do so with a bang! so that is why that is there.

const enteredText = textInputRef.current!.value

Now we just need to send this data to the “App.tsx” file where the “todos” are managed.

Cross-Component Communication

In the “App.tsx” file, add a “todoAddHandler” function with state management.

const todoAddHandler = (text: string) => {
  console.log(text) 
}

Need to make sure the “todoAddHandler” function can be called inside the “NewTodo.tsx” file. We do this by passing a pointer to that component.

// in App.tsx

return (
  <NewTodo onAddTodo={todoAddHandler}/>
  <TodoList items={todos} />
)

And receive that pointer in the “NewTodo.tsx” file.

// in NewTodo.tsx

const todoSubmitHandler = (event: React.FormEvent) => {
  event.preventDefault()
  const enteredText = textInputRef.current!.value
  props.onAddTodo(enteredText)
}

But we need to make sure “props” is passed into the main “NewTodo” function and this throws a TS warning here in the “NewTodo.tsx” file

So we set up an “interface” although we add a “type” here to mix things up.

// in NewTodo.tsx above the main FC

type NewTodoProps = {
  onAddTodo: (todoText: string) => void
}

And now we can pass this right after the “React.FC” in the main FC and the error goes away (as long as we pass “onAddTodo” a string).

const NewTodo: React.FC<NewTodoProps> = props => {...}

Working with State & Types

Manage the “todos” with state in the “App.tsx” file by leveraging “useState” and initializing with an empty array as the initial state.

const [todos, setTodos] = useState([])

Call “setTodos” in the “todoAddHandler” function

const todoAddHandler = (text: string) => {
  setTodos([{id: Math.random().toString(), text: text}])
}

We get an error because when we initialize “useState” with an empty array TS expects to, not get an array, but an empty array specifically. So we need to help TS infer better.

Note: If we initialized “useState” with an empty string and then passed it a string we would not have gotten an error but passing an empty array is what causes us.

So we can adjust the “useState” to be:

const [todos, setTodos] = useState<{id: string, text: string}[]>([])

This works just fine but since we will need it in different places in the app we will instead create an “interface” for the “todos” in its own “todo.model.tsx” file.

export interface Todo {
  id: string
  text: string
}

And import this new file into the “App.tsx” file and pass this as the “type” for the “useState.”

const [todos, setTodos] = useState<Todo[]>([])

But right now we are updating “setTodos” with only one element, overwriting any existing elements which is not what we want. So we need to adjust this logic.

Managing State Better

We don’t want to override state with only the new “todo” but together with the “existing todos” so we could use the spread operator although this is no the cleanest way to implement.

setTodos([...todos, {id: Math.random().toString(), text: text}])

The code above will likely work in all scenarios in this application but theoretically React schedules state updates and therefore what’s in our “…todos” value may not necessarily be the latest state when the state update is performed. So to guarantee this we put a function inside of “setTodos” and receive the existing “todos” as a parameter named “prevtodos” and then returns the new state. This is supported by React. this function will be called by React for us. And then we can rely on “…prevTodos” inside of this function to really be the latest “todos” snapshot.

So this works and now the missing functionality is to delete them.

More Props & State Work

Ensure we can delete “todos” by adding a “span” element and a “button” element to the “todo item” list item in the “map” function in the “TodoList.tsx” file.

// in TodoList.tsx

const ToDoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>
         <span>{todo.text}</span>
         <button onClick={props.onDeleteTodo}>DELETE</button>
        </li>
      ))}
    </ul>
  )
}

And then in the “App.tsx” file, set up a ” todoDeleteHandler” function and pass it as a pointer in the JSX to make sure it can be called inside the “TodoList.tsx” file but TS will not be happy about this because we are not setting a “type” for our props.

// in App.tsx

const todoDeleteHandler(todoId: string) {
  setTodos(prevTodos => {
    return prevTodos.filter(todo => {
      todo.id !== todoId 
    })
  })
}

return (
  <div>
    <NewTodo onAddTodo={todoAddHandler} />
    <ToddoList items={todos} onDeleteTodo={todoDeleteHandler}/>
  </div>
)

So TS will yell at us here so we need to go into the “TodoList.tsx” file and add an “onDeleteTodo” property to the “interface” and set it equal to “(id: string) => void”

// in TodoList.tsx

interface TodoListProps {
  items: {id: string; text: string}[]
  onDeleteTodo: (id: string) => void
}

This removes the error in the “App.tsx” file.

Now on the button in “TodoList.tsx” file, add an “onClick” prop that points to “{props.onDeleteTodo}” as seen in an earlier code block but we are still getting a TS error.

The problem with this is the “todoDeleteHandler” function expects to get the “todo” id as a parameter so we will need to add “.bind(null, todo.id)” although I am not sure why.

// in TodoList.tsx

const ToDoList: React.FC<TodoListProps> = props => {
  return (
    <ul>
      {props.items.map(todo => (
        <li key={todo.id}>
         <span>{todo.text}</span>
         <button onClick={props.onDeleteTodo.bind(null, todo.id)}>DELETE</button>
        </li>
      ))}
    </ul>
  )
}

Adding Styling

Create a new “TodoList.css” file and a “NewTodo.css” file and import into their corresponding “.tsx” files. We paste in the CSS from the course repo.

Types for other React Features (e.g. Redux or Routing)

Hovering over React hooks will give you a heads up on what TS code is looking for. For example, hover over “useEffect” to see with which types it works with.

Redux documentation has a plan for implementing TS, (likely need to do the same for “useImmer” and “useImmerReducer”).

This is easier than it sounds.

Working with React.Router(), installing “–save react-router-dom” has no special type instructions on its page. We can fix with “npm install –save-dev @types/react-router-dom”

Look at the official docs for TS support, if not then check for an “@types” package.

All the TS features can be used in a React project.

Wrap Up

TS adds extra checks and features that make sure we write clear and error-free code. We saw a couple of very important patterns, props, state, etc. Also how to think about adding other functionalities like redux.

Useful Resources & Links