24 / 27Module 2: The React projectBeginner
Project scaffold
This lesson walks through the project scaffold. You will see how the frontend talks to the Rell dapp, with most of the attention on page layout and design.
Overview
The dapp has three main pages:
- Home page: shows the news feed.
- New post page: where a user writes a new post for their feed.
- Users page: lists every user in the system and lets you follow or unfollow them.
Basic layout
Start with a base layout. In src/app/layout.tsx, wrap the {children} element in a main div. Every page renders inside it.
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body className={inter.className}> <main className="flex min-h-screen justify-center bg-gray-100 text-black"> <div className="container">{children}</div> </main> </body> </html> );}The result is a clean, responsive design that looks the same on every page.
Navigation
To move between pages, add a navigation component.
import Link from "next/link";export default function NavBar() { return ( <nav className="py-2 px-6"> <ul className="flex justify-between"> <li> <Link href="/" className="text-xl font-extrabold"> News feed dapp </Link> </li> <div className="flex gap-4 font-semibold"> <li> <Link href="/new-post" className="hover:text-gray-600"> New Post </Link> </li> <li> <Link href="/users" className="hover:text-gray-600"> Users </Link> </li> <li> <Link href="/" className="hover:text-gray-600"> Feed </Link> </li> </div> </ul> </nav> );}Run the app to confirm it works. When you click a link, the URL changes to match.
$ npm run devNews feed (home) page
The home page uses the component in src/components/NewsFeed.tsx:
export default function NewsFeed() { return ( <div className="p-4 md:p-8"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">User name</h1> <div className="flex text-center"> {/* Followers Box */} <div className="bg-white m-1 p-2 rounded-lg shadow"> <h3 className="text-lg font-semibold">Followers</h3> <p className="text-3xl font-bold">0</p> </div> {/* Following Box */} <div className="bg-white m-1 p-2 rounded-lg shadow"> <h3 className="text-lg font-semibold">Following</h3> <p className="text-3xl font-bold">0</p> </div> </div> </div> {/* News Feed */} <div className="bg-white p-4 rounded-lg shadow"> <ul> <li key={0} className="mb-4"> <div className="flex"> <div className="font-semibold">User1</div> <div className="text-gray-500 text-sm ml-2"> {new Date().toLocaleString()} </div> </div> <div className="mt-2">Some content</div> {/* Add a horizontal line between posts */} {<hr className="my-4 border-t border-gray-300" />} </li> </ul> </div> </div> );}New post page
On this page, users write a post for the news feed. The page has a free text field and a button that sends the request.
import { useRouter } from "next/navigation";import { useState } from "react";export default function NewPost() { // Step 1: Initialize state variables const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [content, setContent] = useState(""); // Step 2: Handle text area content change const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { setContent(e.target.value); }; // Step 3: Handle form submission const onSubmit = async (data: string) => { try { if (data.trim() !== "") { setIsLoading(true); // Step 4: Content submission (will be replaced later) router.push("/"); } } catch (error) { console.error(error); } finally { // Step 5: Reset state and loading indicator setContent(""); setIsLoading(false); } }; // Render the component return ( <div className="p-6"> <textarea className="w-full p-2 border rounded" rows={4} placeholder="Write your post..." value={content} onChange={handleContentChange} /> <button className={`${ isLoading ? "bg-gray-500" : "bg-blue-500 hover:bg-blue-600" } w-32 hover:cursor-pointer text-white font-bold py-2 px-4 rounded float-right`} onClick={() => onSubmit(content)} disabled={isLoading} > {isLoading ? "Posting..." : "Post"} </button> </div> );}The code works as follows:
Step 1: Initialize state variables
- Set up the state the component needs:
router: the Next.js router, used for navigation inside the app.isLoading: records whether the component is loading, so the user experience stays smooth.content: stores the text the user types for the post.
Step 2: Handle text area content changes
- The
handleContentChangefunction runs whenever the user types in the text area. It writes the entered text into thecontentstate.
Step 3: Handle form submission
- The asynchronous
onSubmitfunction handles submitting the content. For now it only simulates a submission withrouter.push('/'). Swap this placeholder for your real submission mechanism, for example an API request.
Step 4: Submit content
router.push('/')is a temporary stand-in for submitting content. After you connect the app to a backend server, replace it with your actual submission logic.
Step 5: Reset state and loading indicator
- Once a submission finishes, whether it succeeded or failed,
onSubmitresets the component's state: it empties thecontentfield and setsisLoadingtofalseagain.
Rendering the NewPost component
- The
NewPostcomponent renders:- A text area where the user enters the post content.
- A button that submits the post.
- Visual feedback: the button's look and behavior change according to the
isLoadingstate.
Add the component to your page like this:
import NewPost from "@/components/NewPost";export default function NewPostPage() { return <NewPost />;}User list page
The UserItem component
Each user is shown by the UserItem component. It contains everything needed to display one user: their name and a control to follow or unfollow them.
import { useState } from "react";export type UsersDto = { name: string; id: Buffer;};export default function UserItem({ user }: { user: UsersDto }) { // Step 1: Initialize state variables const [isLoading, setIsLoading] = useState<boolean>(false); const [isFollowing, setIsFollowing] = useState<boolean>(false); // Step 2: Handle follow/unfollow click const handleFollowClick = async (userId: Buffer, follow: boolean) => { try { setIsLoading(true); // Step 3: Handle follow/unfollow logic (Will be replaced later) console.log("Following " + userId.toString("hex") + ": " + follow); setIsFollowing(follow); } catch (error) { console.log(error); } finally { // Step 4: Reset the loading indicator setIsLoading(false); } }; // Render the component return ( <div className="flex justify-between mb-4"> <div className="flex items-center"> {/* User Avatar or Image */} <div className="w-10 h-10 bg-gray-300 rounded-full mr-4 flex justify-center items-center"> {user.name[0]} </div> <span className="text-lg font-semibold">{user.name}</span> </div> <button className={`${ isLoading ? "bg-gray-500" : "bg-blue-500 hover:bg-blue-600" } w-32 hover:cursor-pointer text-white font-bold py-2 px-4 rounded float-right`} disabled={isLoading} onClick={() => handleFollowClick(user.id, !isFollowing)} > {isLoading ? "Loading..." : isFollowing ? "Following" : "Follow"} </button> </div> );}Here is what the component does:
Step 1: Initialize state variables
- The component keeps two pieces of state:
isLoading: shows whether a user-related operation, such as following or unfollowing, is still running.isFollowing: shows whether the logged-in user follows the displayed user, so their follow status is visible right away.
Step 2: Handle follow/unfollow interaction
handleFollowClickhandles following and unfollowing. It runs when the user clicks the follow/unfollow button and setsisLoadingto true while the operation is in progress. This is placeholder logic; replace it with real calls to your backend or database.
Step 3: Update follow status
- For now,
handleFollowClickonly logs the follow/unfollow action. Replace that with real data operations.
Rendering the UserItem component
UserItemrenders:- A user avatar or image: a circle containing the user's initials, so each user is easy to recognize.
- A user name: shown prominently beside the avatar.
- A follow/unfollow button: lets the viewer follow or unfollow the displayed user. Its look and behavior depend on the
isLoadingandisFollowingstate variables, giving instant feedback.
The UsersList component
The UsersList component collects several user items into one list. It renders the users dynamically with the UserItem component.
import UserItem, { UsersDto } from "./UserItem";export type GetUsersReturnType = { pointer: number; users: UsersDto[];};export default function UsersList() { // Define an example array of users const users: GetUsersReturnType | undefined = { pointer: 0, users: [{ name: "User1", id: Buffer.from("AB", "hex") }], }; return ( <div className="p-4 md:p-8"> <ul> {users && users.users.length > 0 ? ( users.users.map((user, index) => ( <li key={user.id.toString()}> {/* Render the UserItem component for each user */} <UserItem user={user} /> {/* Add a horizontal line between user items */} {index < users.users.length - 1 && ( <hr className="my-4 border-t border-gray-300" /> )} </li> )) ) : ( <></> )} </ul> </div> );}How it works:
- The
GetUsersReturnTypetype describes the shape of user data, matching the format returned by the backend or database. - The example uses a placeholder array of users. Replace it with real data from your system.
- The component maps over the user array and renders a
UserItemfor each entry, with horizontal lines between items to keep the list readable.
Last, add the component to the users page:
import UsersList from "@/components/UserList";export default function UsersPage() { return <UsersList />;}These components give your app what it needs to handle user interactions and keep users engaged.