Introduction to React
React is a JavaScript library for building user interfaces, particularly single-page applications. Developed and maintained by Facebook (now Meta), React has become one of the most popular front-end libraries in web development due to its efficiency, flexibility, and component-based architecture.
Core Concepts of React
Component-Based Architecture
At the heart of React is the concept of components. Components are reusable, self-contained pieces of code that return HTML via a render function. React applications are built by composing many components together to create complex user interfaces.
// A simple React component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// Using the component
const element = <Welcome name="Sara" />;
JSX (JavaScript XML)
JSX is a syntax extension to JavaScript that looks similar to HTML. It allows you to write HTML structures in the same file as JavaScript code, making it easier to visualize the UI you're building.
const element = <h1>Hello, world!</h1>;
Behind the scenes, JSX is transformed into regular JavaScript function calls:
const element = React.createElement('h1', null, 'Hello, world!');
Virtual DOM
React uses a Virtual DOM to improve performance. Instead of updating the actual DOM directly when state changes, React:
- Creates a virtual representation of the DOM in memory
- When state changes, creates a new virtual DOM tree
- Compares the new virtual DOM with the previous one (diffing)
- Calculates the minimal number of changes needed to update the real DOM
- Updates only what needs to be changed in the actual DOM
This approach significantly improves performance compared to directly manipulating the DOM for every state change.
One-Way Data Flow
React implements one-way data binding, also known as unidirectional data flow. Data flows from parent components down to child components via props. When state changes in a parent component, the children that depend on that state are re-rendered automatically.
Declarative UI
React uses a declarative paradigm for building UIs. You tell React what state you want the UI to be in, and React ensures the DOM matches that state. This declarative approach makes code more predictable and easier to debug.
Setting Up a React Project
Using Create React App
Create React App is an officially supported way to create single-page React applications with no build configuration.
# Install Create React App globally (if not already installed)
npm install -g create-react-app
# Create a new React application
npx create-react-app my-app
# Navigate to the project directory
cd my-app
# Start the development server
npm start
Using Vite (Modern Alternative)
Vite is a newer, faster build tool that's becoming popular for React development:
# Create a new React project with Vite
npm create vite@latest my-react-app -- --template react
# Navigate to the project directory
cd my-react-app
# Install dependencies
npm install
# Start the development server
npm run dev
React Components
Functional Components
Functional components are JavaScript functions that return JSX. They're simpler and, with the introduction of Hooks, can now handle state and lifecycle methods.
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Arrow function syntax
const Greeting = (props) => {
return <h1>Hello, {props.name}!</h1>;
};
Class Components
Class components are ES6 classes that extend from React.Component and have a render method that returns JSX.
import React, { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
While class components are still supported, functional components with Hooks are now the recommended approach in React.
Props and State
Props
Props (short for properties) are how components receive data from their parent. Props are read-only and help maintain the one-way data flow in React applications.
// Parent component passing props
function App() {
return <Greeting name="John" age={25} />;
}
// Child component receiving props
function Greeting(props) {
return (
<div>
<h1>Hello, {props.name}!</h1>
<p>You are {props.age} years old.</p>
</div>
);
}
State
State is data managed within a component. Unlike props, state can be changed by the component itself.
In class components, state is defined in the constructor:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
In functional components, state is managed using the useState Hook:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
React Hooks
Hooks were introduced in React 16.8 and allow functional components to use state and other React features that were previously only available in class components.
useState
The useState Hook allows functional components to manage state.
import React, { useState } from 'react';
function Example() {
// Declares a state variable called "count" with initial value 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect
The useEffect Hook performs side effects in functional components, similar to lifecycle methods in class components.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
// Optional cleanup function (similar to componentWillUnmount)
return () => {
document.title = 'React App';
};
}, [count]); // Only re-run if count changes
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Other Common Hooks
- useContext: Accesses context without nested component wrappers
- useReducer: Alternative to useState for complex state logic
- useRef: Creates a mutable reference that persists across renders
- useMemo: Memoizes expensive calculations to optimize performance
- useCallback: Returns a memoized callback to prevent unnecessary renders
Custom Hooks
Custom Hooks allow you to extract component logic into reusable functions.
// Custom Hook for form handling
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
// Using the custom Hook
function LoginForm() {
const username = useFormInput('');
const password = useFormInput('');
function handleSubmit(e) {
e.preventDefault();
console.log('Submitted:', username.value, password.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Username" {...username} />
<input type="password" placeholder="Password" {...password} />
<button type="submit">Login</button>
</form>
);
}
JSX in Depth
JavaScript Expressions in JSX
You can embed JavaScript expressions in JSX by using curly braces {}.
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
// You can use more complex expressions
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = <h1>Hello, {formatName(user)}!</h1>;
Conditional Rendering
There are several ways to handle conditional rendering in React:
// Using if statements
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
// Using ternary operators
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <UserGreeting /> : <GuestGreeting />}
</div>
);
}
// Using logical && operator
function Mailbox({ unreadMessages }) {
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 && (
<h2>You have {unreadMessages.length} unread messages.</h2>
)}
</div>
);
}
Lists and Keys
When rendering lists in React, you should include a "key" prop for each item:
function NumberList({ numbers }) {
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return <ul>{listItems}</ul>;
}
Keys help React identify which items have changed, added, or removed. Keys should be unique among siblings and stable across renders.
Handling Events
React events are named using camelCase and pass event handlers as functions:
function Button() {
function handleClick(e) {
e.preventDefault();
console.log('Button was clicked');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
Passing parameters to event handlers:
function Button({ id, text }) {
const handleClick = (id, e) => {
console.log(`Button ${id} was clicked`);
};
return (
<button onClick={(e) => handleClick(id, e)}>
{text}
</button>
);
}
Forms in React
In HTML, form elements maintain their own state. In React, form state is typically controlled by the component:
function NameForm() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`A name was submitted: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
This is called a "controlled component" because React controls both the value and the changes to the value.
Component Lifecycle
Class Component Lifecycle
Class components have lifecycle methods that execute at different stages of a component's life:
-
Mounting:
constructor()static getDerivedStateFromProps()render()componentDidMount()
-
Updating:
static getDerivedStateFromProps()shouldComponentUpdate()render()getSnapshotBeforeUpdate()componentDidUpdate()
-
Unmounting:
componentWillUnmount()
Functional Component Lifecycle with Hooks
For functional components with Hooks, the lifecycle can be managed with useEffect:
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// Similar to componentDidMount
useEffect(() => {
console.log('Component mounted');
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Similar to componentWillUnmount
return () => {
console.log('Component will unmount');
clearInterval(interval);
};
}, []); // Empty dependency array means run once on mount
// Similar to componentDidUpdate for specific props/state
useEffect(() => {
console.log('Seconds updated:', seconds);
}, [seconds]); // Run when seconds changes
return <div>Seconds: {seconds}</div>;
}
Context API
Context provides a way to pass data through the component tree without having to pass props down manually at every level:
// Create a context
const ThemeContext = React.createContext('light');
// Provider in parent component
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</ThemeContext.Provider>
);
}
// Intermediate component
function Toolbar() {
return <ThemedButton />;
}
// Consumer in a deeply nested component
function ThemedButton() {
const theme = useContext(ThemeContext);
return <Button theme={theme} />;
}
React Router
React Router is the standard routing library for React. It enables navigation between views in your application:
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:userId" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
State Management
Local Component State
For simple applications, using useState and prop drilling may be sufficient.
Context API + useReducer
For mid-sized applications, combining Context with useReducer can provide a Redux-like state management solution:
// Create context
const TodoContext = React.createContext();
// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
}
// Provider component
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// Consumer component
function TodoList() {
const { todos, dispatch } = useContext(TodoContext);
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
);
}
Redux
For larger applications, Redux provides a predictable state container:
// Actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
// Action creators
function addTodo(text) {
return { type: ADD_TODO, text };
}
function toggleTodo(id) {
return { type: TOGGLE_TODO, id };
}
// Reducer
function todoReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{ id: Date.now(), text: action.text, completed: false }
];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
}
// Store
const store = Redux.createStore(todoReducer);
// Connect to React with react-redux
function TodoList({ todos, toggleTodo }) {
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
);
}
const mapStateToProps = state => ({
todos: state
});
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Server Communication
Fetching Data with useEffect
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
setUsers(data);
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
React Query
React Query simplifies data fetching, caching, and state management:
import { useQuery, useMutation, QueryClient, QueryClientProvider } from 'react-query';
// Set up the client
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
function UserList() {
const { isLoading, error, data } = useQuery('users', () =>
fetch('https://api.example.com/users').then(res => res.json())
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Testing React Components
Jest and React Testing Library
React Testing Library focuses on testing components from the end user's perspective:
// Button.jsx
function Button({ onClick, children }) {
return (
<button onClick={onClick} data-testid="custom-button">
{children}
</button>
);
}
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByTestId('custom-button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
Performance Optimization
React.memo
Prevents unnecessary re-renders for functional components:
const MemoizedComponent = React.memo(function MyComponent(props) {
// Only re-renders if props change
return <div>{props.name}</div>;
});
useMemo and useCallback
Memoize expensive calculations and callbacks:
function SearchResults({ query, data }) {
// Memoize expensive calculation
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(item => item.name.includes(query));
}, [data, query]);
// Memoize callback
const handleItemClick = useCallback((id) => {
console.log(`Clicked item ${id}`);
}, []);
return (
<ul>
{filteredData.map(item => (
<li key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Code Splitting
Split your code into smaller chunks that load on demand:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load components
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
React Ecosystem
UI Component Libraries
- Material-UI: React components that implement Google's Material Design
- Chakra UI: Simple, modular and accessible component library
- Ant Design: Enterprise-level UI design language
- Tailwind CSS: Utility-first CSS framework that works well with React
State Management
- Redux: Predictable state container for JavaScript apps
- MobX: Simple, scalable state management
- Recoil: Experimental state management library by Facebook
- Zustand: Small, fast and scalable state management solution
Form Libraries
- Formik: Complete solution for forms
- React Hook Form: Performance-focused form library
- Final Form: High performance subscription-based form state management
Data Fetching
- React Query: Powerful data fetching and caching library
- SWR: React Hooks for remote data fetching
- Apollo Client: Complete state management library for GraphQL
Styling Solutions
- Styled-components: CSS-in-JS library
- Emotion: CSS-in-JS library designed for high performance
- CSS Modules: Locally scoped CSS classes
Meta-Frameworks
- Next.js: Framework for server-rendered React applications
- Remix: Newer framework focused on web fundamentals and modern UX
- Gatsby: Static site generator for React
Best Practices
- Keep components small and focused on a single responsibility
- Use functional components with Hooks for most use cases
- Lift state up to the nearest common ancestor when multiple components need the same data
- Use prop destructuring for cleaner component definitions
- Use fragments to avoid unnecessary DOM nodes
- Properly handle component keys in lists
- Leverage code splitting for better performance
- Follow file/folder organization that scales with your project
- Create reusable custom Hooks for shared logic
- Write tests for your components
Getting Started with React
- Learn the fundamentals: JSX, components, props, state
- Master Hooks: useState, useEffect, useContext, etc.
- Build small projects: Todo list, calculator, etc.
- Explore routing with React Router
- Learn state management: Context API, Redux, etc.
- Study forms and data validation
- Practice API integration
- Explore testing with Jest and React Testing Library
React's versatility and strong ecosystem make it an excellent choice for building modern web applications, from simple interfaces to complex enterprise applications. Its component-based architecture promotes reusability and maintainability, while its virtual DOM implementation ensures good performance.