Table of Contents
- Understanding React’s Core Paradigm
- Setting Up Your React Environment
- JSX: The HTML-in-JS Syntax
- Components: The Building Blocks of React
- Props: Passing Data Between Components
- State: Managing Dynamic Data
- Lifecycle & Side Effects with Hooks
- Advanced Hooks: useContext, useReducer, and More
- Routing with React Router
- State Management Beyond useState
- Best Practices for React Development
- Conclusion
- References
1. Understanding React’s Core Paradigm
Before diving into code, it’s critical to grasp how React differs from vanilla JavaScript. Here are the key mental models:
Declarative vs. Imperative
- Vanilla JS is imperative: You write step-by-step instructions to manipulate the DOM (e.g.,
document.getElementById('root').innerHTML = 'Hello'). - React is declarative: You describe what the UI should look like for a given state, and React handles how to update the DOM.
Example: Updating a counter
// Vanilla JS (Imperative)
let count = 0;
const button = document.querySelector('button');
const display = document.querySelector('span');
button.addEventListener('click', () => {
count++;
display.textContent = count; // Manually update DOM
});
// React (Declarative)
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Component-Based Architecture
React apps are built from reusable, independent components (e.g., Button, Card, Navbar). Components can be nested, making UIs modular and easy to maintain.
Virtual DOM
React uses a lightweight in-memory representation of the DOM called the Virtual DOM. When state changes, React compares the Virtual DOM with the real DOM (a process called “reconciliation”) and updates only the necessary parts—optimizing performance.
2. Setting Up Your React Environment
To start building React apps, you’ll need:
- Node.js (v14.0.0+ recommended) and npm (or Yarn) for package management.
- A code editor (VS Code with the ES7+ React Snippets extension is popular).
Option 1: Create React App (CRA)
CRA is a zero-configuration toolchain for bootstrapping React apps:
npx create-react-app my-first-react-app
cd my-first-react-app
npm start
This spins up a development server at http://localhost:3000 with hot reloading.
Option 2: Vite (Faster Alternative)
Vite is a modern build tool with faster startup and HMR (Hot Module Replacement). Use it for larger projects:
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
3. JSX: The HTML-in-JS Syntax
JSX (JavaScript XML) is React’s syntax for writing HTML-like code within JavaScript. It’s not HTML, but a syntax extension that gets transpiled to JavaScript (via Babel) before runtime.
Key JSX Rules
-
Expressions in JSX: Use
{}to embed JavaScript expressions (variables, functions, calculations).const name = "Alice"; <h1>Hello, {name}!</h1> {/* Renders: <h1>Hello, Alice!</h1> */} -
Attributes: Use camelCase for HTML attributes (e.g.,
classNameinstead ofclass,onClickinstead ofonclick).<div className="container" onClick={handleClick}>Click me</div> -
Self-closing tags: Tags without children (e.g.,
<img>,<input>) must close with/>.<img src="logo.png" alt="React Logo" /> -
Single root element: JSX expressions must return a single root element. Use fragments (
<></>or<React.Fragment>) to group elements without adding extra DOM nodes:// Valid (with fragment) <> <h1>Title</h1> <p>Paragraph</p> </> // Invalid (multiple roots) <h1>Title</h1> <p>Paragraph</p>
Why JSX?
JSX makes code more readable than vanilla JS’s document.createElement calls. Compare:
// Vanilla JS
const element = document.createElement('div');
element.className = 'greeting';
element.textContent = `Hello, ${name}`;
// React with JSX
const element = <div className="greeting">Hello, {name}</div>;
4. Components: The Building Blocks
React components are reusable, self-contained pieces of UI. There are two types: functional components (modern, preferred) and class components (legacy).
Functional Components
A functional component is a JavaScript function that returns JSX. It accepts props (data) as input and returns what to render.
Example: A Simple Greeting Component
// Greeting.js
function Greeting() {
return <h1>Hello, React!</h1>;
}
export default Greeting;
To use it in another component:
// App.js
import Greeting from './Greeting';
function App() {
return (
<div className="App">
<Greeting /> {/* Renders the Greeting component */}
</div>
);
}
Class Components (Legacy)
Class components use ES6 classes and render() methods. They’re less common today (replaced by functional components and hooks), but you may encounter them in older code:
class Greeting extends React.Component {
render() {
return <h1>Hello, React!</h1>;
}
}
5. Props: Passing Data Between Components
Props (short for “properties”) are how components pass data to each other. Think of props as function arguments for components.
Passing and Accessing Props
To pass props, add attributes to the component when rendering it. To access props, use the props parameter in functional components.
Example: Passing a Name to a Component
// User.js (Child component)
function User(props) {
return <p>Welcome, {props.name}!</p>; // Access props.name
}
// App.js (Parent component)
function App() {
return (
<div>
<User name="Alice" /> {/* Pass "Alice" as a prop */}
<User name="Bob" /> {/* Pass "Bob" as a prop */}
</div>
);
}
Output:
Welcome, Alice!
Welcome, Bob!
Destructuring Props
For cleaner code, destructure props directly in the function parameters:
function User({ name }) { // Destructure props.name
return <p>Welcome, {name}!</p>;
}
Default Props
Set default values for props if they’re not provided:
function User({ name = "Guest" }) { // Default to "Guest"
return <p>Welcome, {name}!</p>;
}
// Usage: <User /> → "Welcome, Guest!"
6. State: Managing Dynamic Data
In vanilla JavaScript, variables update, but the DOM doesn’t automatically reflect changes. React’s state solves this: it’s a built-in way to manage dynamic data that triggers UI re-renders when updated.
Introducing the useState Hook
Hooks are functions that let you “hook into” React state and lifecycle features from functional components. The useState hook is used to declare state variables.
Syntax:
const [stateVariable, setStateFunction] = useState(initialValue);
stateVariable: The current state value.setStateFunction: A function to update the state (triggers re-render).initialValue: The initial value of the state (optional).
Example: Counter Component
import { useState } from 'react';
function Counter() {
// Declare a state variable "count" with initial value 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
- When the “Increment” button is clicked,
setCount(count + 1)updatescount. - React re-renders the
Countercomponent with the newcountvalue, updating the UI.
State vs. Vanilla JS Variables
A vanilla JS variable won’t trigger a re-render when updated. Only state variables (declared with useState) cause React to re-render the component.
7. Lifecycle & Side Effects with Hooks
In vanilla JS, you might use DOMContentLoaded or event listeners for setup/cleanup. In React, side effects (e.g., data fetching, subscriptions, manually updating the DOM) are handled with the useEffect hook.
The useEffect Hook
useEffect lets you perform side effects in functional components. It replaces legacy class lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
Syntax:
useEffect(() => {
// Effect code (runs after render)
return () => {
// Cleanup code (runs before re-render or unmount)
};
}, [dependencies]); // Re-run effect only if dependencies change
Common Use Cases
1. Run Once on Mount (Like componentDidMount)
Pass an empty dependencies array ([]) to run the effect once after the initial render:
useEffect(() => {
console.log("Component mounted!");
}, []); // Empty array → runs once
2. Data Fetching
Fetch data from an API and update state:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user data when userId changes
const fetchUser = async () => {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
};
fetchUser();
// Cleanup: Abort fetch if component unmounts or userId changes
return () => {
// (Optional) Use AbortController for fetch cleanup
};
}, [userId]); // Re-run effect when userId changes
if (!user) return <p>Loading...</p>;
return <div>Name: {user.name}</div>;
}
3. Cleanup Side Effects
Use the cleanup function to avoid memory leaks (e.g., removing event listeners, canceling subscriptions):
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup: Remove event listener when component unmounts
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array → runs once, cleanup on unmount
8. Advanced Hooks
Beyond useState and useEffect, React provides several advanced hooks for common patterns.
useContext: Share State Across Components
useContext lets components access context (global state) without prop drilling (passing props through multiple levels).
Example: Theme Context
// ThemeContext.js
import { createContext, useContext, useState } from 'react';
// 1. Create a context
const ThemeContext = createContext();
// 2. Create a provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children} {/* Children can access context */}
</ThemeContext.Provider>
);
}
// 3. Custom hook to use the context
export function useTheme() {
return useContext(ThemeContext);
}
Now any component can access the theme:
import { useTheme } from './ThemeContext';
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#333' : '#fff' }}
onClick={toggleTheme}
>
Toggle Theme ({theme})
</button>
);
}
useReducer: Complex State Logic
For state logic with multiple sub-values or complex transitions, useReducer is a better alternative to useState. It works like Redux’s reducer pattern.
Example: Todo List
import { useReducer } from 'react';
// Reducer function: Takes current state and action, returns new state
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
default:
return state;
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []); // Initial state: empty array
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'Learn useReducer' })}>
Add Todo
</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
9. Routing with React Router
Single-page applications (SPAs) use client-side routing to navigate without reloading the page. React Router is the de facto library for this.
Setup
Install React Router:
npm install react-router-dom
Key Components
BrowserRouter: Wraps your app to enable routing (usually inindex.js).Routes: A container forRoutecomponents.Route: Maps a URL path to a component.Link: Navigation link (replaces<a>tags to avoid full page reloads).
Example: Basic Routing
1. Wrap your app with BrowserRouter (in index.js):
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
2. Define routes in App.js:
import { Routes, Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';
function App() {
return (
<div>
{/* Navigation */}
<nav>
<Link to="/">Home</Link> |{' '}
<Link to="/about">About</Link> |{' '}
<Link to="/contact">Contact</Link>
</nav>
{/* Routes */}
<Routes>
<Route path="/" element={<Home />} /> {/* Home page */}
<Route path="/about" element={<About />} /> {/* About page */}
<Route path="/contact" element={<Contact />} /> {/* Contact page */}
</Routes>
</div>
);
}
10. State Management Beyond useState
For apps with complex state (e.g., user sessions, cart data), useState and useContext may not be enough. Here are common solutions:
Context API + useReducer
Ideal for medium-sized apps needing global state (e.g., theme, user auth). Combine useContext and useReducer to manage global state cleanly.
Redux
A predictable state container for large apps. It uses a single store, actions, and reducers to manage state. Libraries like react-redux and @reduxjs/toolkit simplify setup.
Zustand/Jotai
Lightweight alternatives to Redux with less boilerplate. Zustand, for example, lets you create a store with a simple hook:
// store.js
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// Use in component
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
11. Best Practices for React Development
1. Component Composition Over Inheritance
Use component composition (passing components as props) instead of inheritance for code reuse:
// Parent component passing a child component
function Card({ children }) {
return <div className="card">{children}</div>;
}
// Usage
<Card>
<h2>Card Title</h2>
<p>Card content</p>
</Card>
2. Avoid Unnecessary Re-renders
- Use
React.memoto memoize components that don’t need to re-render. - Use the
useCallbackhook to memoize functions passed as props. - Use the
useMemohook to memoize expensive calculations.
3. Keep Components Small and Focused
A component should do one thing. Split large components into smaller, reusable ones.
4. Use Functional Components and Hooks
Class components are legacy. Stick to functional components and hooks for cleaner, more maintainable code.
5. Test Your Components
Use tools like Jest (testing framework) and React Testing Library (component testing) to write unit tests:
// Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('increments count when button is clicked', () => {
render(<Counter />);
const button = screen.getByText('Increment');
fireEvent.click(button);
expect(screen.getByText('You clicked 1 times')).toBeInTheDocument();
});
Conclusion
Transitioning from JavaScript to React involves shifting from an imperative, DOM-manipulating mindset to a declarative, component-based one. By mastering core concepts like JSX, props, state, hooks, and routing, you’ll be able to build dynamic, scalable UIs efficiently.
Start small (e.g., a todo app or weather dashboard), experiment with hooks, and gradually explore advanced topics like state management and testing. With practice, React will feel like a natural extension of your JavaScript skills.
References
- React Official Documentation
- React Router Documentation
- MDN Web Docs: JavaScript
- Kent C. Dodds’ Blog (React patterns and best practices)
- Redux Documentation
- Zustand Documentation