TypeScript Learning Adventures: A Tale of Love and Hate - React and TypeScript
React JS is a very well-liked JavaScript library for creating incredible user interfaces. Additionally, it works with TypeScript. Although it's not the default, TypeScript is...
React JS is a very well-liked JavaScript library for creating incredible user interfaces. Additionally, it works with TypeScript.
Although it's not the default, TypeScript is used in the majority of significant React projects. In this post, you'll learn how it works.
Now, a brief disclaimer: I won't teach React's fundamentals in this article. This article is for you if you already know React and want to learn how to use TypeScript in React.
Project setup
To build a React app with Typescript, we need a project setup.
The setup should handle both our React code and TypeScript at the same time. For example, JSX can handle our React code by compiling it to JavaScript, while also optimizing it.
Lucky for us there is a CreateReactApp, a tool provided by the React team. We can use it to create React applications, and it supports TypeScript out of the box. Setting up such a project on our own can be somewhat challenging, so we will use CreateReactApp to save time.
There, you'll discover how to use CreateReactApp and TypeScript to start a new project or add TypeScript to an existing one. We'll create a new project in our case, so we'll paste in the following command:
npx create-react-app book-management-app --template typescript
It scaffolds a new project into this folder. And in that project, a React application is created so that we may all write our code using TypeScript.
You might see that it has the same structure as a React App you develop without TypeScript. But the tsconfig.json
file is already visible. By the way, you can change this file to fit your requirements, which I explained in the previous article. The majority of use cases are covered by the default configuration.
We also have access to some .tsx
files in the src folder, which is where we will write our source code. These .tsx
files are here because these are files where you can write TypeScript code. But also you can write all the JSX code too. JSX is a special JavaScript syntax related to React. There you write HTML markup inside of your JavaScript. Or in this case, TypeScript code.
Now, we can already see some TypeScript syntax in these files. For instance, here, we have a type assignment in the index.tsx
file:
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
And with that, we have a foundation upon which to build.
How does TypeScript work in React?
Our development server can be started with npm start
command. We should leave it running so it can watch our files for updates.
It will also recompile the different sorts of scripts (.js, .ts, .tsx) into JavaScript. So if you save a file after modification, it will do so. Finally, your React application will be built and served on the local host on port 3000.
When you run the app, you won't see anything special. We are currently only rendering the default React logo in the App.tsx
file, but let's change it to this:
import React from 'react';
const App: React.FC = () => {
return (
<div className="App">Hello world!</div>
);
};
export default App;
This is regular JavaScript code. We see this type of assignment though, and there, what we can see is that we assigned this strange React.FC
type to App
. Now, if we ignore that type for a second, what's actually stored in App
?
A function, yes, an arrow function, but it's still a function nonetheless. Under the hood, this is a type made available by the react types package, which is now crucial.
If you navigate to the node_modules
folder, you'll notice that there are many types there. As well as all the react-dom
types here and in the @types
folder. But what kind is this FC
type, then?
This FC
is shorthand for a football club.
Just kidding, it's not!
It's shorthand for a function component. You may use a longer version of this in its place. That is the same kind. The second one is a shortcut that states that the data we save in this App
must be a function. But one that is eligible to be used as a function component in React.
Of course, you write class-based components as well. But this is a function component now since it produces JSX, which is how a function component in React is produced.
We will experience a problem if I remove this return statement. Because it would prevent the compilation from starting. Now that we've told TypeScript that we want to put a function component here, we would encounter an error. Because all that is stored is a regular function and not a function that returns JSX would be considered a react element.
As a result, this would be incorrect, and we would need to make it right. Thus, we can already observe how TypeScript enhances our project in this area. It provides extra type safety and ensures that we can't operate if, for instance, we build an incorrect component.
About the possibility that you would ever forget this return statement. Keep in mind that you are creating a larger component containing a variety of if statements and return statements.
TypeScript can rescue your ass. It can give you a warning a little bit earlier than during runtime. Where it may otherwise crash in some circumstances if you forget one in one branch of your if statement.
Aside from that now we'll work on the front end of the book reading manager app whose back end we completed in the prior post. We won't make any API calls; instead, we'll use a basic array to hold the data.
Props and prop types
The goal is to make a book list component, so we'll make a new directory called "components" in the source folder. Next, we put the bookList.tsx
file there to hold code for the book list component:
import React from "react";
const BookList: React.FC = () => {
const books = [{ id: "1", title: "Adrenaline", writer: "Zlatan Ibrahimovic" }];
return (
<ul>
<li>
{books.map((book) => (
<li key={book.id}>
{book.title} - {book.writer}
</li>
))}
</li>
</ul>
);
};
export default BookList;
Still, this is not how you want your components to be. It might be acceptable for a first attempt.
So, let's change that in our App.tsx
file as the component will receive data from outside:
import React from "react";
import BookList from "../components/bookList";
function App() {
const books = [{ id: "1", title: "Adrenaline", writer: "Zlatan Ibrahimovic" }];
return (
<div className="App">
<BookList books={books} />
</div>
);
}
export default App;
and our BookList
should look like this:
import React from "react";
interface Book {
id: string;
title: string;
writer: string;
}
interface BookListProps {
books: Book[];
}
const BookList: React.FC<BookListProps> = (props) => {
const { books } = props;
return (
<ul>
<li>
{books.map((book) => (
<li key={book.id}>
{book.title} - {book.writer}
</li>
))}
</li>
</ul>
);
};
export default BookList;
Take note of the following:
- TypeScript allows us to design an interface that details our prop types. We use an array of books as our prop type in the
BookList
component because we receive an array of books in this case. - Since
React.FC
is a generic type, we may specify any type there to instruct our component's data structure for props. - VS Code will immediately report an error if the books parameter isn't passed to the
BookList
component in theApp.tsx
file. - We also receive the autocomplete feature when props are defined in this way. This means that if I type something wrong, like
props.book
instead ofprops.books
, VS Code will give us an error.
So we get a lot of safety with using TypeScript here.
Book input management
We can create a new component called AddBook
to allow users to add new books.
To manage user input we can use React refs.
React refs are straightforward pointers to HTML DOM elements. They allow us to handle them and can be used to manage user input. The kind of HTML DOM element you are using the ref on must be specified when using TypeScript.
As an illustration, it is set to HTMLInputElement
here because we will use it on the input field. But it can also be HTMLSelectElement
, HTMLParagraphElement
, etc.
import React, { useRef } from "react";
interface AddBookProps {
onAddBookHandler: (title: string, writer: string) => void;
}
const AddBook: React.FC<AddBookProps> = (props) => {
const titleRef = useRef<HTMLInputElement>(null);
const writerRef = useRef<HTMLInputElement>(null);
const submitBookHandler = (event: React.FormEvent) => {
event.preventDefault();
const title = titleRef.current?.value;
const writer = writerRef.current?.value;
console.log({ title, writer });
if (!writer || !title) {
return;
}
props.onAddBookHandler(title, writer);
};
return (
<form onSubmit={submitBookHandler}>
<div>
<label htmlFor="title"></label>
<input id="title" type="text" ref={titleRef} />
</div>
<div>
<label htmlFor="writer"></label>
<input id="writer" type="text" ref={writerRef} />
</div>
<button type="submit">Add book</button>
</form>
);
};
export default AddBook;
You can define props for this component by defining it in React.FC
type, as we did before. As we get data from user input, we also use refs, which have a special react type of HTMLInputElement
.
Additionally, this component gets a props method called onAddBookHandler
. That function adds a new book to our backend and it takes 2 arguments (title and writer).
When a user clicks submit, the submitBookHandler
function receives a FormEvent
event. At that point, we validate the data before sending it.
Component communication
In a genuine project, your state management and component communication will take place in a centralized location.
So let's implement the deletion and update of the read state of books in our BookList
component:
import React from "react";
import { Book } from "../bookModel";
interface BookListProps {
books: Book[];
onDeleteBookHandler: (bookId: string) => void;
onToggleReadBookHandler: (bookId: string) => void;
}
const BookList: React.FC<BookListProps> = (props) => {
const { books, onDeleteBookHandler, onToggleReadBookHandler } = props;
return (
<ul>
{books.map((book) => (
<li key={book.id}>
<span>
{book.title} - {book.writer}
</span>
<button onClick={() => onDeleteBookHandler(book.id)}>Delete</button>
<button onClick={() => onToggleReadBookHandler(book.id)}>
Mark as {book.isRead ? "not read" : "read"}
</button>
</li>
))}
</ul>
);
};
export default BookList;
With this, we can also apply changes to our App.tsx
file:
import React, { useState } from "react";
import BookList from "./components/bookList";
import AddBook from "./components/addBook";
import { Book } from "./bookModel";
function App() {
const [books, setBooks] = useState<Book[]>([]);
const addBookHandler = (title: string, writer: string) => {
setBooks((prevBooks) => [
...books,
{ id: Math.random().toString(), title, writer, isRead: false }
]);
};
const deleteBookHandler = (bookId: string) => {
setBooks((prevBooks) => prevBooks.filter((book) => book.id !== bookId));
};
const toggleReadBookHandler = (bookId: string) => {
setBooks((prevBooks) =>
prevBooks.map((book) => (book.id === bookId ? { ...book, isRead: !book.isRead } : book))
);
};
return (
<div className="App">
<AddBook onAddBookHandler={addBookHandler} />
<BookList
books={books}
onDeleteBookHandler={deleteBookHandler}
onToggleReadBookHandler={toggleReadBookHandler}
/>
</div>
);
}
export default App;
As you can see, we removed the hard-coded data and are now storing it using the useState
hook. useState
is also flexible and supports any form of data you want to store. We use the same syntax as before to describe the type of data we will store in the state. And this is where we have a variety of books stored.
We provide the component with the deleteBookHandler
function to allow for the deletion of books. This function accepts the book ID. Then, using the .filter
function, we replace the array that already contains books with that ID with a new one that doesn't.
The toggleReadBookHandler
function does the same thing. There, we change the read state of a single instance of the book to a new boolean value. This is accomplished using the .map
method. There we determine whether the book ID matches the one we want to update. If the answer is yes, we update the isRead
property with the new value, with the spread operator. If the answer is no, we use the original object.
And that's it, our little project is finished. The entire project code is available here.
Conclusion
So, we examined React and TypeScript in this article.
As you can see, TypeScript can offer many useful extra features. It also offers a lot of tests that ensure we produce clear and error-free code.
Now, I did show you a few patterns there, as well as how to leverage some key React capabilities like props and state, in combination with TypeScript.
Keep in mind everything else you learned about TypeScript. All the types you have studied, and everything TypeScript is capable of. Everything you learned applies to this TypeScript and React project.
Even though we're using JSX and React, we're still writing JavaScript. Or more particularly TypeScript. All the information you get in this article series is applicable here as well.
It's simple to forget that, so be mindful of it at all times. After that, think about using TypeScript to create your next React project. You now understand how to begin with it and how it operates.
Comments ()