Chromia
Start Building

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:

  1. Home page: shows the news feed.
  2. New post page: where a user writes a new post for their feed.
  3. 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.

src/app/layout.tsx
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.

To move between pages, add a navigation component.

src/components/NavBar.tsx
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.

shell
$ npm run dev

News feed (home) page

The home page uses the component in src/components/NewsFeed.tsx:

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.

src/components/NewPost.tsx
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 handleContentChange function runs whenever the user types in the text area. It writes the entered text into the content state.

Step 3: Handle form submission

  • The asynchronous onSubmit function handles submitting the content. For now it only simulates a submission with router.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, onSubmit resets the component's state: it empties the content field and sets isLoading to false again.

Rendering the NewPost component

  • The NewPost component 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 isLoading state.

Add the component to your page like this:

src/app/new-post/page.tsx
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.

src/components/UserItem.tsx
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

  • handleFollowClick handles following and unfollowing. It runs when the user clicks the follow/unfollow button and sets isLoading to 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, handleFollowClick only logs the follow/unfollow action. Replace that with real data operations.

Rendering the UserItem component

  • UserItem renders:
    • 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 isLoading and isFollowing state 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.

src/components/UserList.tsx
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 GetUsersReturnType type 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 UserItem for each entry, with horizontal lines between items to keep the list readable.

Last, add the component to the users page:

src/app/users/page.tsx
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.

Written for this site from Chromia’s public course. Code samples are the course’s own.