In the previous article, we set up the project: we installed React, TypeScript, and Tailwind CSS.
Now let's start diving into React itself. And we'll begin with components.
React as a Templating Engine
What are templating engines? When working with plain HTML, we might run into repeating structures, for example, user cards:
<div class="user-card">
<img src="..." alt="Alex">
<h2>Alex</h2>
<p>Backend developer</p>
</div>
<div class="user-card">
<img src="..." alt="Maria">
<h2>Maria</h2>
<p>Designer</p>
</div>
<div class="user-card">
<img src="..." alt="John">
<h2>John</h2>
<p>Web developer</p>
</div>
If we wanted to change the name style, we'd have to change it in every card. Templating engines allow us to break HTML into reusable elements (components).
We can pass data into components (to distinguish one user card from another). The components themselves can also be broken down into other components, forming a hierarchy.
You can think of components as custom HTML tags, for example:
<UserCard name="Maria" avatar="..." position="Designer"/>
A component can be anything from a small button to an entire section on a page.
Let's practice and create a page with comments. First, create a Components folder inside the src folder, and inside it, a Comment.tsx file. So the file path is src/Components/Comment.tsx.
function Comment() {
return (
<div>Text of comment</div>
);
}
export default Comment
We simply created a function and exported it. If you're not familiar with import/export yet, here's an article about them.
Next, modify the src/App.tsx file:
import Comment from './Components/Comment.tsx'
function App() {
return <>
<Comment />
</>
}
export default App
Run the server if it's not already running, and look at our page:
npm run dev
We should see "Text of comment" on the page.
So, a component is simply a function that returns something that looks a lot like HTML. This thing is called JSX, and under the hood, it gets transformed into a special JavaScript object.
Also, it's important that the component's name (our function) starts with a capital letter. By the way, App is also a component.
Now let's make our example a bit more complex. First, change App.tsx:
import {Comment} from "./Components/Comment.tsx";
function App() {
return <>
<h2 className="text-2xl font-bold text-center text-gray-800">Comments:</h2>
<div className="max-w-2xl mx-auto p-4">
<Comment
avatarUrl="https://i.pravatar.cc/40?img=26"
userName="Maria"
createdAt="2 hours ago"
content="Text of the comment"
likeCount={14}
/>
</div>
</>
}
export default App
In essence, JSX is very similar to regular HTML, but there are some differences. First, we use <></> tags here.
According to React rules, a component must have only one root tag. We could have used a div instead, but empty tags allow us to avoid creating unnecessary wrappers.
Also, instead of the class attribute, we use className (because class is a reserved word). We're using class names from Tailwind.
Also, the like count is wrapped in curly braces. That's because what's inside curly braces is treated as a JavaScript expression. If we had written likeCount="14", "14" would have been treated as a string (and TypeScript's main purpose is to ensure type consistency).
Now let's change the comment component in Comment.tsx:
import React from 'react';
import { Avatar } from './Avatar';
import { CommentHeader } from './CommentHeader';
import { CommentContent } from './CommentContent';
import { CommentActions } from './CommentActions';
interface CommentProps {
avatarUrl: string;
userName: string;
createdAt: string;
content: string;
likeCount?: number;
}
export const Comment: React.FC<CommentProps> = ({
avatarUrl,
userName,
createdAt,
content,
likeCount = 0,
}) => {
return (
<div className="flex gap-3 p-4 mb-4 border border-gray-200 rounded-lg shadow-sm">
<Avatar src={avatarUrl} alt={userName} />
<div className="flex-1">
<CommentHeader userName={userName} createdAt={createdAt} />
<CommentContent content={content} />
<CommentActions likeCount={likeCount} />
</div>
</div>
);
};
First, we import some components that we'll create shortly (meaning there should be an error right now - an attempt to import non-existent components). Then we declare the type for props. Props are the data passed into the component.
Looking ahead, if we pass data from a parent component, we cannot change it directly inside the child component. Next, we immediately export our component (remember, a component is a function). React.FC<CommentProps> is the function's type (its signature - argument types and return type).
Strictly speaking, it's not mandatory to specify this type, because TypeScript can infer the return type automatically. However, when passing props to components, it's highly recommended to specify the type so the IDE knows the structure of the props object.
Thus, in export const Comment: React.FC<CommentProps> = (...) => {...}, we're exporting a constant of type React.FC<CommentProps> and assigning it a function/component.
I hope you're familiar with arrow functions in JavaScript (yes, a component can also be an arrow function).
Next, we use destructuring syntax to unpack the props. Finally, we return JSX. Inside it, we use other components (which we'll create very soon) and pass arguments (props) that we received from the App component.
Now it's time to create the other components. Create these files:
src/Components/Avatar.tsx
import React from 'react';
interface AvatarProps {
src: string;
alt: string;
}
export const Avatar: React.FC<AvatarProps> = ({ src, alt }) => {
return (
<img
src={src}
alt={alt}
className="w-10 h-10 rounded-full object-cover flex-shrink-0"
/>
);
};
src/Components/CommentContent.tsx
import React from 'react';
interface CommentContentProps {
content: string;
}
export const CommentContent: React.FC<CommentContentProps> = ({ content }) => {
return <p className="text-sm text-gray-800 mb-2">{content}</p>;
};
src/Components/CommentHeader.tsx
import React from 'react';
interface CommentHeaderProps {
userName: string;
createdAt: string;
}
export const CommentHeader: React.FC<CommentHeaderProps> = ({
userName,
createdAt,
}) => {
return (
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-sm">{userName}</span>
<span className="text-xs text-gray-500">{createdAt}</span>
</div>
);
};
src/Components/CommentActions.tsx
import React from 'react';
interface CommentActionsProps {
likeCount: number;
}
export const CommentActions: React.FC<CommentActionsProps> = ({ likeCount }) => {
return (
<button
className="flex items-center gap-1 text-sm text-gray-500 hover:text-blue-600 transition-colors cursor-pointer"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<span>{likeCount}</span>
</button>
);
};
The icon is from heroicons.
Now we can go to the page and see how it looks. And we can easily add another comment component in App.tsx, right below the first one, but with different data.
<Comment
avatarUrl="https://i.pravatar.cc/40?img=8"
userName="Jhon"
createdAt="50 minutes ago"
content={"Text of the second comment"}
likeCount={10}
/>
So, we've learned to use React to break HTML into reusable components. But using React only for this is like buying a high-end TV just for the weather forecast. In the next article, we'll uncover the true power of React.
Top comments (0)