Master modern web development with React, the popular JavaScript library for building user interfaces, and enhance it with TypeScript for robust, scalable applications.
**React.js** is a free and open-source front-end JavaScript library for building user interfaces based on components. It's maintained by Meta and a community of individual developers and companies. React allows developers to create large web applications that can change data without reloading the page. Its main goal is to be fast, scalable, and simple.
**React.ts** refers to using React with TypeScript. As you learned in the TypeScript course, TypeScript adds static typing to JavaScript, which means you can define the types of your props, state, and other variables. This leads to fewer runtime errors, better tooling support (autocompletion, refactoring), and improved maintainability, especially in larger codebases.
React (UI) + TypeScript (Safety) = Powerful Web Apps!
We'll use **Vite**, a fast build tool that provides a quicker development experience for React projects, especially when combined with TypeScript.
React development requires Node.js and a package manager (npm or Yarn). If you haven't already, install them first.
node -v
npm -v
Open your terminal or VS Code's integrated terminal and run the following commands:
# Create a new Vite project with React + TypeScript template
npm create vite@latest my-react-ts-app -- --template react-ts
# Navigate into your new project directory
cd my-react-ts-app
# Install dependencies
npm install
# Start the development server
npm run dev
After running `npm run dev`, Vite will provide a local URL (e.g., `http://localhost:5173/`). Open this URL in your web browser to see your React app running.
Once the project is created, open it in VS Code:
code .
This command opens the current directory in VS Code.
With the development server running (`npm run dev`), any changes you save in your `.tsx` files within VS Code will automatically trigger a live reload in your browser.
This setup provides a powerful and efficient development workflow for React with TypeScript!
React applications are built using **components**, which are independent, reusable pieces of UI. **Props** (short for properties) are how you pass data from a parent component to a child component.
`src/components/Greeting.tsx`:
// src/components/Greeting.tsx
import React from 'react';
// Define the types for the component's props
interface GreetingProps {
name: string;
age?: number; // Optional prop
}
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div className="p-4 bg-blue-100 rounded-md">
<p>Hello, {name}!</p>
{age && <p>You are {age} years old.</p>}
</div>
);
};
export default Greeting;
`src/App.tsx` (using the component):
// src/App.tsx
import React from 'react';
import Greeting from './components/Greeting'; // Make sure path is correct
function App() {
return (
<div className="text-center p-4">
<h1 className="text-2xl font-bold mb-4">Component Example</h1>
<Greeting name="Alice" />
<Greeting name="Bob" age={30} />
</div>
);
}
export default App;
Hello, Alice!
Hello, Bob!
You are 30 years old.
**State** is data that a component can manage and change over time. When state changes, React re-renders the component. The `useState` hook allows functional components to have state.
`src/App.tsx` (or a new component):
// In App.tsx or a new component like Counter.tsx
import React, { useState } from 'react';
function Counter() {
// Declare a state variable 'count' and a function 'setCount' to update it
const [count, setCount] = useState<number>(0); // Type 'number' for count
const increment = () => {
setCount(count + 1);
};
return (
<div className="p-4 bg-green-100 rounded-md text-center">
<p className="text-lg">Count: {count}</p>
<button
onClick={increment}
className="mt-2 px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
>
Increment
</button>
</div>
);
}
export default Counter;
Count: 0
The `useEffect` hook lets you perform side effects in functional components. Common side effects include data fetching, subscriptions, or manually changing the DOM. It runs after every render by default, but you can control when it runs using the dependency array.
`src/App.tsx` (or a new component):
// In App.tsx or a new component like Timer.tsx
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState<number>(0);
useEffect(() => {
// This effect runs once after the initial render, and cleans up on unmount
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Cleanup function: runs when the component unmounts or before the effect re-runs
return () => clearInterval(interval);
}, []); // Empty dependency array means this effect runs ONCE on mount
return (
<div className="p-4 bg-blue-100 rounded-md text-center">
<p className="text-lg">Seconds: {seconds}</p>
</div>
);
}
export default Timer;
Seconds: 0
In React, you can render different elements or components based on certain conditions. Common patterns include `if/else` statements, ternary operators, and logical `&&`.
Example:
import React, { useState } from 'react';
const ConditionalMessage: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
return (
<div className="p-4 bg-indigo-100 rounded-md text-center">
{isLoggedIn ? (
<p className="text-green-700">Welcome back!</p>
) : (
<p className="text-red-700">Please log in.</p>
)}
<button
onClick={() => setIsLoggedIn(!isLoggedIn)}
className="mt-2 px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600"
>
{isLoggedIn ? 'Logout' : 'Login'}
</button>
</div>
);
};
export default ConditionalMessage;
Please log in.
To render a list of items, you typically use JavaScript's `map()` method on an array. Each item in a list should have a unique `key` prop. Keys help React identify which items have changed, are added, or are removed, improving performance and avoiding bugs.
Example:
import React from 'react';
interface Item {
id: number;
name: string;
}
const ItemList: React.FC = () => {
const items: Item[] = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
];
return (
<div className="p-4 bg-teal-100 rounded-md">
<h3 className="text-lg font-semibold mb-2">Fruits:</h3>
<ul className="list-disc list-inside">
{items.map(item => (
<li key={item.id} className="mb-1">
{item.name}
</li>
))}
</ul>
</div>
);
};
export default ItemList;
React events are named using camelCase (e.g., `onClick` instead of `onclick`). You pass a function as the event handler. TypeScript helps by providing types for event objects.
Example:
import React, { useState } from 'react';
const InteractiveForm: React.FC = () => {
const [text, setText] = useState<string>('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Prevent default form submission behavior
alert("Submitted: " + text); // Changed to string concatenation
};
return (
<form onSubmit={handleSubmit} className="p-4 bg-pink-100 rounded-md">
<input
type="text"
value={text}
onChange={handleChange}
placeholder="Type something..."
className="p-2 border rounded-md w-full mb-2"
/>
<button
type="submit"
className="px-4 py-2 bg-pink-500 text-white rounded-md hover:bg-pink-600"
>
Submit
</button>
</form>
);
};
export default InteractiveForm;
The Context API provides a way to pass data through the component tree without having to pass props down manually at every level (prop drilling). It' ideal for global data like themes, user authentication, or preferred language.
Example (`ThemeContext`, `ThemeProvider`, `ThemeSwitcher`):
// In a separate file, e.g., src/contexts/ThemeContext.tsx
import React, { createContext, useState, useCallback, useMemo } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = useCallback(() => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
}, []);
const contextValue = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
};
// In a component that consumes the context, e.g., src/components/ThemeSwitcher.tsx
import React, { useContext } from 'react';
// import { ThemeContext } from '../contexts/ThemeContext'; // Adjust path
const ThemeSwitcher: React.FC = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('ThemeSwitcher must be used within a ThemeProvider');
}
const { theme, toggleTheme } = context;
return (
<div className={"p-4 rounded-lg shadow-md " + (theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-800')}>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
// In src/App.tsx to use it:
// import { ThemeProvider } from './contexts/ThemeContext';
// import ThemeSwitcher from './components/ThemeSwitcher';
// function App() {
// return (
// <ThemeProvider>
// <ThemeSwitcher />
// </ThemeProvider>
// );
// }
Current Theme: light
The `useReducer` hook is an alternative to `useState` for more complex state logic, especially when the next state depends on the previous one or when the state involves multiple sub-values. It' often used with a "reducer" function, similar to Redux.
Example (`CounterWithReducer`):
// In a new component, e.g., src/components/CounterWithReducer.tsx
import React, { useReducer } from 'react';
interface CounterState {
count: number;
}
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' };
const counterReducer = (state: CounterState, action: CounterAction): CounterState => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
// For exhaustive type checking in TypeScript, you might add:
// const exhaustiveCheck: never = action;
// return exhaustiveCheck;
return state; // Fallback for unknown action types
}
};
const CounterWithReducer: React.FC = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div className="p-4 bg-yellow-50 rounded-lg shadow-md">
<p className="text-lg font-semibold">Count: {state.count}</p>
<div className="flex space-x-2 mt-2">
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
</div>
);
};
export default CounterWithReducer;
Count: 0
Custom Hooks are JavaScript functions whose names start with "use" and that can call other Hooks. They let you extract reusable stateful logic from a component, making it easier to share logic between components without duplicating code.
Example (`useFetch` hook and `PostFetcher` component):
// In a separate file, e.g., src/hooks/useFetch.ts
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
// Only fetch if window is defined (client-side)
if (typeof window !== 'undefined') {
fetchData();
}
}, [url]);
return { data, loading, error };
}
export default useFetch;
// In a component that uses the hook, e.g., src/components/PostFetcher.tsx
import React from 'react';
// import useFetch from '../hooks/useFetch'; // Adjust path
interface Post {
id: number;
title: string;
body: string;
}
const PostFetcher: React.FC = () => {
// Using the custom hook to fetch data
const { data, loading, error } = useFetch<Post>('https://jsonplaceholder.typicode.com/posts/1');
if (loading) return <p>Loading post...</p>;
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>;
return (
<div className="p-4 bg-green-100 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-green-800">Fetched Post:</h3>
{data && (
<div>
<p className="font-bold">{data.title}</p>
<p className="text-sm text-gray-700">{data.body}</p>
</div>
)}
</div>
);
};
export default PostFetcher;
Loading post...
Refs provide a way to access DOM nodes or React elements created in the render method. They are typically used for managing focus, text selection, media playback, or integrating with third-party DOM libraries. The `useRef` hook returns a mutable ref object whose `.current` property is initialized to the passed argument.
Example:
import React, { useRef } from 'react';
const TextInputWithFocusButton: React.FC = () => {
// Create a ref object, typed for an HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// Access the DOM node through .current and call its methods
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div className="p-4 bg-blue-100 rounded-md">
<input
type="text"
ref={inputRef} // Attach the ref to the input element
placeholder="Click button to focus me"
className="p-2 border rounded-md w-full mb-2"
/>
<button
onClick={focusInput}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
>
Focus Input
</button>
</div>
);
};
export default TextInputWithFocusButton;
React provides several tools to optimize component re-renders and prevent unnecessary computations:
Example (`memo`):
import React, { useState, memo } from 'react';
interface ExpensiveComponentProps {
count: number;
}
// Memoize this component to prevent re-renders if 'count' prop doesn't change
const ExpensiveComponent = memo(({ count }: ExpensiveComponentProps) => {
console.log('Rendering ExpensiveComponent'); // Check console for this log
// Simulate an expensive calculation
let sum = 0;
for (let i = 0; i < 100000; i++) { // Reduced loop for faster demo
sum += i;
}
return (
<div className="p-4 bg-purple-100 rounded-lg shadow-md">
<p>Expensive Component Rendered! Count: {count}</p>
<p className="text-xs text-gray-600">Simulated sum: {sum}</p>
</div>
);
});
const ParentComponent: React.FC = () => {
const [memoCount, setMemoCount] = useState<number>(0);
const [nonMemoValue, setNonMemoValue] = useState<string>('');
return (
<div className="p-4 bg-purple-50 rounded-md">
<h3 className="text-lg font-semibold mb-2">Performance Demo</h3>
<button
onClick={() => setMemoCount(memoCount + 1)}
className="px-4 py-2 bg-purple-500 text-white rounded-md hover:bg-purple-600 mr-2"
>
Increment Memo Count ({memoCount})
</button>
<input
type="text"
value={nonMemoValue}
onChange={(e) => setNonMemoValue(e.target.value)}
placeholder="Type here (causes parent re-render)"
className="p-2 border rounded-md"
/>
<ExpensiveComponent count={memoCount} />
<p className="text-sm mt-2 text-gray-700">
Type in the input field above. Notice the "Expensive Component Rendered!" log only appears when the "Increment Memo Count" button is clicked,
because the <code>ExpensiveComponent</code> is memoized and its <code>count</code> prop only changes when that button is clicked.
</p>
</div>
);
};
export default ParentComponent;
Expensive Component Rendered! Count: 0
Sum calculated: 4999999950000000
Type in the input field above. Notice the "Expensive Component Rendered!" log only appears when the "Increment Memo Count" button is clicked, because the ExpensiveComponent
is memoized and its count
prop only changes when that button is clicked.
Error Boundaries are React components that **catch JavaScript errors anywhere in their child component tree,** log those errors, and display a fallback UI instead of the crashed component tree. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. They are class components with `static getDerivedStateFromError()` or `componentDidCatch()`.
Example (Conceptual - requires a class component):
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// You can also log the error to an error reporting service
console.error("Uncaught error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
// In src/App.tsx (or any parent component):
// import ErrorBoundary from './components/ErrorBoundary';
// const BuggyComponent: React.FC = () => {
// throw new Error('I crashed!');
// return <p>This won't render</p>;
// };
// function App() {
// return (
// <ErrorBoundary fallback={<p>Something went wrong!</p>}>
// <BuggyComponent />
// </ErrorBoundary>
// );
// }
Error Boundaries are conceptual here as they require a class component and would crash the preview if directly implemented with a throwing component. The code above shows the structure.
Simulated Error Boundary: Something went wrong!
TypeScript significantly enhances React development by providing static type checking, which catches errors early, improves code readability, and enables better IDE support. Here' how TypeScript is typically applied in React:
Define an `interface` or `type` for your component' props to specify what data it expects.
// Define props interface
interface UserCardProps {
userName: string;
userAge: number;
isVerified: boolean;
}
// Use React.FC (or just direct function type) with the props interface
const UserCard: React.FC<UserCardProps> = ({ userName, userAge, isVerified }) => {
return (
<div>
<h3>{userName}</h3>
<p>Age: {userAge}</p>
{isVerified && <p>Verified User ✅</p>}
</div>
);
};
// Usage:
// <UserCard userName="Jane Doe" userAge={28} isVerified={true} />
// <UserCard userName="John Doe" userAge="twenty" isVerified={false} /> // TypeScript error!
Provide a type argument to `useState` to explicitly define the type of your state variable.
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoList: React.FC = () => {
// State is an array of Todo objects
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodoText, setNewTodoText] = useState<string>('');
const addTodo = () => {
if (newTodoText.trim() === '') return;
const newTodo: Todo = {
id: Date.now(),
text: newTodoText,
completed: false,
};
setTodos([...todos, newTodo]);
setNewTodoText('');
};
return (
<div>
<input type="text" value={newTodoText} onChange={(e) => setNewTodoText(e.target.value)} />
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
};
React' synthetic events have specific types provided by TypeScript (e.g., `React.ChangeEvent`, `React.MouseEvent`).
const MyButton: React.FC = () => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Button clicked!", event.currentTarget.tagName);
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log("Input value changed:", event.target.value);
};
return (
<div>
<button onClick={handleClick}>Click Me</button>
<input type="text" onChange={handleInputChange} placeholder="Type here" />
</div>
);
};
When creating custom hooks, define the types for their arguments and return values.
import { useState, useEffect } from 'react';
// Define the return type of the hook
interface WindowSize {
width: number;
height: number;
}
function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// Usage:
// const MyComponent: React.FC = () => {
// const { width, height } = useWindowSize();
// return <p>Window size: {width}x{height}</p>;
// };
You've completed the React.js to React.ts Fundamentals to Advanced Course! You now have a solid understanding of building interactive user interfaces with React, leveraging its component-based architecture, state management, and lifecycle effects. Furthermore, you've learned how TypeScript enhances this process by providing type safety and improving code quality.
Keep practicing by building more complex projects. Consider exploring React Router for navigation, dedicated state management libraries like Redux Toolkit or Zustand, and unit testing with tools like React Testing Library and Jest!