The full source code for the ReactSparkPortfolio is available on GitHub . As I work on the project, GitHub pages will be the place to view the application: ReactSpark Portfolio Site
Fast development with modern tooling
I am currently working on a team that develops with React Native and TypeScript for both web and mobile. To date, most of my work has been in C#, on the API side of things. Working with web APIs, Microsoft SQL Server, and Azure Cloud Services has been my bread and butter for the past few years. I wanted to expand my skill set and learn more about what fellow developers where using. I started with Vite, a next-generation front-end build tool. Vite is fast, lean, and highly configurable.
React Native, with its component-based architecture, felt a bit like working with C# classes but with a twist. Each React component is a self-contained unit, much like a class, which you can compose together to build complex UIs.
The addition of TypeScript was particularly comforting, as it brought in type safety and a clear structure to the code, reducing the likelihood of bugs and making the codebase easier to maintain.
I have enjoyed learning about React Native with TypeScript. It combines the flexibility of JavaScript for mobile and web development with the robust type-checking of TypeScript, creating a powerful toolkit for building scalable and maintainable applications. It's been a rewarding journey to expand my skills and experience.
I started by using Vite's project scaffolding for React. This sets up the initial folder structure and installs dependencies. I then navigated to the project folder and installed the project dependencies.
npm create vite@latest
cd ReactSparkPortfolio
npm install
After setting up the project, I created components for my portfolio. I wanted a simple, clean layout with sections for Home, About, Projects, and Joke.
import React, { useState } from 'react';
import Header from './components/Header';
import Hero from './components/Hero';
import About from './components/About';
import Projects from './components/Projects';
import Joke from './components/Joke';
import Footer from './components/Footer';
const App: React.FC = () => {
const [activeSection, setActiveSection] = useState<string>('home');
const handleSectionChange = (section: string) => {
setActiveSection(section);
};
return (
<div className="d-flex flex-column min-vh-100">
<Header onSectionChange={handleSectionChange} />
<main className="flex-grow-1 pt-5 mt-5 container">
<div className="row justify-content-center">
<div className="col-md-10">
{activeSection === 'home' && <Hero />}
{activeSection === 'about' && <About />}
{activeSection === 'projects' && <Projects />}
{activeSection === 'joke' && <Joke />}
</div>
</div>
</main>
<Footer />
</div>
);
};
export default App;
Vite builds the production files into a "dist" folder by default. For GitHub Pages, I modified Vite's configuration to output into the "docs" folder. This makes setting up the GitHub Pages deployment easier as it serves the site from the "docs" folder by default.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
base: "./",
build: {
outDir: "docs",
},
plugins: [react()],
});
To display content from an RSS feed, create an "Articles" component that fetches and parses the RSS XML file, rendering article titles and links.
This TypeScript code defines a React functional component that dynamically fetches and displays a list of articles from an RSS feed. The component uses React's useState hook to create a state variable that holds the articles, which is initialized as an empty array. The state is updated once the RSS feed is fetched and parsed.
Additionally, useEffect is used to ensure that the fetch operation happens when the component first renders. Since useEffect is invoked with an empty dependency array, it acts like a componentDidMount lifecycle event, executing the fetch operation only once when the component is mounted.
import React, { useEffect, useState } from 'react';
const Articles: React.FC = () => {
const [articles, setArticles] = useState<any[]>([]);
useEffect(() => {
const fetchRSSFeed = async () => {
const response = await fetch('/rss.xml');
const rssData = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(rssData, 'application/xml');
const items = xmlDoc.getElementsByTagName('item');
const parsedArticles = Array.from(items).map(item => ({
title: item.getElementsByTagName('title')[0].textContent,
link: item.getElementsByTagName('link')[0].textContent,
pubDate: item.getElementsByTagName('pubDate')[0].textContent,
}));
setArticles(parsedArticles);
};
fetchRSSFeed();
}, []);
return (
<div>
<h2>Latest Articles</h2>
<ul>
{articles.map((article, index) => (
<li key={index}>
<a href={article.link}>{article.title}</a> - {article.pubDate}
</li>
))}
</ul>
</div>
);
};
export default Articles;
Deploying a Vite site to GitHub Pages involves configuring a GitHub Actions workflow. This automates the process and pushes the build output to the "docs" folder on every push.
name: Deploy React App to GitHub Pages
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build the project
run: npm run build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
permissions:
contents: write
Vite simplified React development with features like instant hot module replacement and minimal configuration. With just a few steps, I was able to deploy a project using GitHub Pages. Vite offers flexible solutions that help you get your project online quickly. Now that I got the initial project completed and deployed, I can focus on adding more features and content to the project as I learn more about React Native with TypeScript.
Explore TypeScript concepts through examples and interactive code
Type annotations in TypeScript allow developers to explicitly define the data types for variables, function arguments, and return values. This proactive approach helps catch potential errors early in the development process, promoting the use of correct data types and improving overall code stability. Unlike JavaScript, which is more flexible with data types, TypeScript enforces stricter rules, bringing more predictability and helping to prevent unexpected bugs.
In the context of React Native, type annotations play a critical role in managing components and their props. By ensuring props are correctly typed, they help reduce runtime errors. Type annotations also simplify state management and API interactions by enforcing data structures and identifying mismatches early on. Overall, they contribute to a smoother development experience, especially with TypeScript's helpful autocompletion and type inference features.
const App: React.FC = () => {
// State to keep track of which section is active
const [activeSection, setActiveSection] = useState<string>('home');
// Handler to update the active section based on navigation clicks
const handleSectionChange = (section: string) => {
setActiveSection(section);
};
return (
<div className="d-flex flex-column min-vh-100">
<Header onSectionChange={handleSectionChange} />
<main className="flex-grow-1 pt-5 mt-5 container">
<div className="row justify-content-center">
<div className="col-md-10">
{activeSection === 'home' && <Hero />}
{activeSection === 'about' && <About />}
{activeSection === 'projects' && <Projects />}
{activeSection === 'contact' && <Contact />}
</div>
</div>
</main>
<Footer />
</div>
);
}
Functions with typed parameters and return types are essential in TypeScript for making sure that your code is robust and less prone to errors. This is particularly useful in a React Native app, where type safety can help you avoid many common bugs.
import React from 'react';
import { View, Text } from 'react-native';
// Define a type for the function parameters
type GreetingProps = {
name: string;
age: number;
};
// Define the function with typed parameters and return type
const Greet: React.FC<GreetingProps> = ({ name, age }: GreetingProps): JSX.Element => {
return (
<View>
<Text>Hello, my name is {name} and I am {age} years old.</Text>
</View>
);
};
// Usage of the function in a React component
const App: React.FC = () => {
return (
<View>
<Greet name="Alice" age={30} />
</View>
);
};
export default App;
By using typed parameters and return types, you ensure that your components receive the expected types, reducing the chance of bugs and making your code easier to maintain. Plus, TypeScript provides auto-completion and type-checking features, which makes development more efficient.
In TypeScript, function parameters can be optional or have default values. Optional and default parameters in TypeScript provide a way to make functions and components more flexible and robust:
In Hero.tsx, the concept of optional and default parameters is applied to enhance flexibility and robustness. The component accepts an optional profileData parameter, allowing it to function seamlessly even when this data isn't provided.
import React from 'react';
import defaultProfile from '../data/profile.json';
interface Profile {
name: string;
introduction: string;
ctaLink: string;
ctaText: string;
}
const renderHero = (profileData?: Profile) => {
const profile = profileData || defaultProfile;
if (!profile || Object.keys(profile).length === 0) {
return (
<section id="hero" className="bg-secondary text-white text-center py-5">
<div className="container">
<div className="row justify-content-center">
<div className="col-md-8">
<h1 className="display-4">Error</h1>
<p className="lead">Profile data is missing or unavailable.</p>
</div>
</div>
</div>
</section>
);
}
return (
<section id="hero" className="bg-secondary text-white text-center py-5">
<div className="container">
<div className="row justify-content-center">
<div className="col-md-8">
<h1 className="display-4">{profile.name}</h1>
<p className="lead">{profile.introduction}</p>
<a href={profile.ctaLink} className="btn btn-primary btn-lg mt-4">
{profile.ctaText}
</a>
</div>
</div>
</div>
</section>
);
};
const Hero: React.FC<{ profileData?: Profile }> = ({ profileData }) => {
return renderHero(profileData);
};
export default Hero;
TypeScript interfaces are powerful tools that define the structure of an object, detailing the expected properties and their types. They enforce type-checking, ensuring that any object adhering to the interface meets these criteria, thus reducing runtime errors. In the Articles.tsx file, the Article interface outlines the structure for an article object, specifying that each article should have a title, link, and pubDate, all of which are strings.
This interface is then used in the state management, ensuring that the articles state array contains objects that conform to this structure. By defining the shape of the article data upfront, the code becomes more predictable and easier to maintain, providing clear expectations for the data being handled.
interface Article {
title: string;
link: string;
pubDate: string;
}
Type aliases allow you to define custom types, which can make your code more readable and reusable. A Type Alias in TypeScript provides a new name for any type, like a shorthand or simplification. This could be a primitive type, a union of types, an object type, or any other structure.
The keyword type is used to create these aliases, essentially giving a new name to an existing type or a complex structure. This is especially useful for making your code cleaner and more understandable, particularly when dealing with intricate type definitions.
type Article = {
title: string;
link: string;
pubDate: string;
};
TypeScript's Union and Intersection types add flexibility and power to type definitions, particularly useful in complex React applications.
import React from 'react';
type Admin = {
adminRights: string[];
};
type User = {
name: string;
email: string;
};
type SuperUser = Admin & User;
const UserProfile: React.FC<{ user: User | SuperUser }> = ({ user }) => {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
{'adminRights' in user && (
<ul>
{user.adminRights.map((right) => (
<li key={right}>{right}</li>
))}
</ul>
)}
</div>
);
};
export default UserProfile;
Generics in TypeScript provide a way to create reusable components that work with a variety of types instead of a single one. They allow you to write flexible and type-safe code by enabling you to define components, functions, or hooks that can operate with different data types while preserving type information.
Example in a Web Application: Let's say you have a list component that can render lists of different types, such as users, products, or orders. By using generics, you can create a single List component that works for any type of data.
import React from 'react';
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
const users = [{ name: 'Alice' }, { name: 'Bob' }];
const products = [{ name: 'Laptop' }, { name: 'Phone' }];
const App: React.FC = () => {
return (
<div>
<h2>Users:</h2>
<List items={users} renderItem={(user) => <span>{user.name}</span>} />
<h2>Products:</h2>
<List items={products} renderItem={(product) => <span>{product.name}</span>} />
</div>
);
};
export default App;
TypeScript type assertions are a way to tell the compiler that you know more about the type of a value than it does. It's essentially a way to override the compiler's inferred type and specify a more specific type, which can be useful in certain scenarios.
Imagine you have an input field where users can enter their age, and you want to ensure that the value is treated as a number. Since the input value is always a string by default, you can use a type assertion to convert it to a number.
import React, { useState } from 'react';
const AgeInput: React.FC = () => {
const [age, setAge] = useState<number | null>(null);
const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputAge = e.target.value as unknown as number;
setAge(inputAge);
};
return (
<div>
<label htmlFor="age">Enter your age:</label>
<input type="text" id="age" onChange={handleAgeChange} />
{age !== null && <p>Your age is: {age}</p>}
</div>
);
};
export default AgeInput;
Enums, or enumerations, in TypeScript are a way to define a set of named constants. They can be a great tool to make your code more readable and expressive, especially when dealing with sets of related values.
Example in a Web Application: Suppose you are building a task management app where each task can have a specific status.
import React, { useState } from 'react';
enum TaskStatus {
ToDo = 'ToDo',
InProgress = 'InProgress',
Done = 'Done'
}
interface Task {
id: number;
title: string;
status: TaskStatus;
}
const TaskComponent: React.FC<{ task: Task }> = ({ task }) => {
const [status, setStatus] = useState<TaskStatus>(task.status);
const handleStatusChange = (newStatus: TaskStatus) => {
setStatus(newStatus);
};
return (
<div>
<h3>{task.title}</h3>
<p>Status: {status}</p>
<button onClick={() => handleStatusChange(TaskStatus.ToDo)}>To Do</button>
<button onClick={() => handleStatusChange(TaskStatus.InProgress)}>In Progress</button>
<button onClick={() => handleStatusChange(TaskStatus.Done)}>Done</button>
</div>
);
};
const App: React.FC = () => {
const task: Task = {
id: 1,
title: 'Learn TypeScript enums',
status: TaskStatus.ToDo
};
return <TaskComponent task={task} />;
};
export default App;
React Native, like React, allows components to receive data and event handlers through props. Using TypeScript with React Native adds the benefit of strong typing to these props, enhancing code quality and reducing bugs.
interface GreetingProps {
name: string;
age?: number;
}
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<View>
<Text>Hello, {name}!</Text>
{age && <Text>You are {age} years old.</Text>}
</View>
);
};
TypeScript classes are a way to create reusable blueprints for objects, encapsulating both data and behavior. They bring an object-oriented approach to TypeScript, much like classes in C#.
Classes also allow you to define a constructor, a special method used for creating and initializing objects of the class. TypeScript classes can include methods that define behaviors related to the class's data and support inheritance for code reuse and creating specialized classes.
class Article {
title: string;
link: string;
pubDate: string;
constructor(title: string, link: string, pubDate: string) {
this.title = title;
this.link = link;
this.pubDate = pubDate;
}
// Method to get a formatted article summary
getSummary(): string {
return `${this.title} was published on ${this.pubDate}. Read more at ${this.link}`;
}
}
// Example usage
const article = new Article('New TypeScript Release', 'https://typescriptlang.org', '2024-10-12');
console.log(article.getSummary());
These TypeScript code samples cover core features that will improve the development experience for new developers. They ensure safer, more predictable code, especially in React Native projects where maintaining clarity in component structures is essential.
I decided to implement React Router for navigation between sections using URLs like /#about and /#projects.
Here are the steps I took to integrate React Router into my portfolio project. This enabled me to navigate to different sections using URLs like /#about and /#projects.
The first step is to install the React Router library by running the following command:
npm install react-router-dom
Next, update the `App.tsx` file to use `HashRouter` for URL navigation. The following code sets up routes for different sections of your portfolio:
import { HashRouter, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import { lazy, Suspense } from 'react';
const Hero = lazy(() => import('./components/Hero'));
const About = lazy(() => import('./components/About'));
const Projects = lazy(() => import('./components/Projects'));
const Articles = lazy(() => import('./components/Articles'));
const Joke = lazy(() => import('./components/Joke'));
const App: React.FC = () => {
return (
<HashRouter>
<div className="d-flex flex-column min-vh-100">
<Header />
<main className="flex-grow-1 pt-5 mt-5 container">
<div className="row justify-content-center">
<div className="col-md-10">
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Hero />} />
<Route path="/about" element={<About />} />
<Route path="/projects" element={<Projects />} />
<Route path="/joke" element={<Joke />} />
<Route path="/articles" element={<Articles />} />
</Routes>
</Suspense>
</div>
</div>
</main>
<Footer />
</div>
</HashRouter>
);
};
export default App;
Modify the `Header.tsx` file to replace traditional anchor `<a>` tags with React Router's `Link` component for route-based navigation:
import { Link } from 'react-router-dom';
const Header: React.FC = () => {
return (
<header>
<nav className="navbar navbar-expand-lg navbar-light bg-light fixed-top">
<div className="container-fluid">
<Link className="navbar-brand" to="/">Mark Hazleton</Link>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item">
<Link className="nav-link" to="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/about">About</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/projects">Projects</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/articles">Articles</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/joke">Joke</Link>
</li>
</ul>
</div>
</div>
</nav>
</header>
);
};
export default Header;
Finally, build the project with the following command:
npm run clean
npm run build
npm run dev
Now I have added React Router to the portfolio project. With this setup, users can navigate different sections using hash-based URLs without reloading the page.
Deploying a Vite project to GitHub Pages can be straightforward, but requires attention to configuration, especially around base paths and environment variables. This guide provides clear steps to successfully deploy your Vite app.
In Vite, the base path must be set to match the subdirectory where GitHub Pages serves the site. This ensures correct paths to assets like JavaScript and CSS. In the `vite.config.ts`, configure the base property:
export default defineConfig({
base: process.env.VITE_BASE_URL || "/",
build: {
outDir: "docs",
},
});
Use environment variables for path configuration. Create `.env` and `.env.production` files to manage paths in development and production environments. The production file should reflect your repository name.
.env file:
VITE_BASE_URL=/
.env.production file:
VITE_BASE_URL=/repository-name/
Vite automatically loads environment variables with the `VITE_` prefix. For custom environment handling, import the `dotenv` package and manually load environment files based on the environment mode.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import * as dotenv from "dotenv";
const mode = process.env.NODE_ENV || "development";
dotenv.config({ path: `.env.${mode}` });
export default defineConfig({
base: process.env.VITE_BASE_URL || "/",
build: {
outDir: "docs",
},
plugins: [react()],
});
After configuring the project, run the build command to generate the production-ready files in the `docs/` folder, which is used by GitHub Pages.
npm run build
Once the build is complete, commit and push the `docs/` folder to your repository. In the GitHub Pages settings, set the source to the `docs/` folder to make your site live.
git add .
git commit -m "Deploy Vite project to GitHub Pages"
git push origin main
By following these steps, you can deploy a Vite project to GitHub Pages with proper handling of environment variables and paths. The critical step is setting the correct base path and using dotenv to manage environment settings across development and production environments.