React
Panduan lengkap React modern - components, hooks, state management, dan best practices
Pengenalan
React adalah library JavaScript yang powerful untuk membangun user interfaces. courses ini akan mengajarkan Anda fundamental React modern menggunakan functional components dan hooks, serta best practices untuk membangun aplikasi yang scalable dan maintainable.
Setup Project
Create React App
# Menggunakan Vite (recommended)
bun create vite my-app --template react
cd my-app
bun install
bun run dev
# Atau menggunakan Create React App
bunx create-react-app my-app
cd my-app
bun startProject Structure
my-app/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── Button.jsx
│ │ └── Card.jsx
│ ├── hooks/
│ │ └── useAuth.js
│ ├── utils/
│ │ └── helpers.js
│ ├── App.jsx
│ ├── main.jsx
│ └── index.css
├── package.json
└── vite.config.jsComponents
Functional Components
// Basic component
function Welcome() {
return <h1>Hello, World!</h1>;
}
// Component with props
function Greeting({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old</p>
</div>
);
}
// Component with default props
function Button({ text = 'Click me', onClick }) {
return <button onClick={onClick}>{text}</button>;
}
// Component with children
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
</Card>
);
}JSX
// JSX expressions
function UserInfo({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
<p>Status: {user.isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}
// Conditional rendering
function Greeting({ isLoggedIn, username }) {
if (isLoggedIn) {
return <h1>Welcome back, {username}!</h1>;
}
return <h1>Please sign in</h1>;
}
// Ternary operator
function Status({ isOnline }) {
return (
<div>
Status: {isOnline ? '🟢 Online' : '🔴 Offline'}
</div>
);
}
// Logical && operator
function Notification({ hasNotifications, count }) {
return (
<div>
{hasNotifications && (
<span className="badge">{count}</span>
)}
</div>
);
}
// Lists and keys
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}
// Fragments
function UserProfile() {
return (
<>
<h1>Profile</h1>
<p>User information</p>
</>
);
}
// Inline styles
function StyledComponent() {
const styles = {
container: {
padding: '20px',
backgroundColor: '#f0f0f0'
},
title: {
fontSize: '24px',
color: '#333'
}
};
return (
<div style={styles.container}>
<h1 style={styles.title}>Styled Component</h1>
</div>
);
}Hooks
useState
import { useState } from 'react';
// Basic state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
);
}
// Object state
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="age"
type="number"
value={user.age}
onChange={handleChange}
placeholder="Age"
/>
</form>
);
}
// Array state
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos(prev => [
...prev,
{ id: Date.now(), text: input, completed: false }
]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
// Lazy initialization
function ExpensiveComponent() {
const [data, setData] = useState(() => {
// Expensive calculation only runs once
return computeExpensiveValue();
});
return <div>{data}</div>;
}useEffect
import { useState, useEffect } from 'react';
// Basic effect
function DocumentTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // Runs when count changes
return (
<button onClick={() => setCount(count + 1)}>
Increment
</button>
);
}
// Fetch data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Event listeners
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array = run once on mount
return (
<div>
Window size: {size.width} x {size.height}
</div>
);
}
// Interval/Timer
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let interval;
if (isRunning) {
interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isRunning]);
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Start'}
</button>
<button onClick={() => setSeconds(0)}>Reset</button>
</div>
);
}useContext
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
const UserContext = createContext();
// Theme provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for theme
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Component using theme
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme (Current: {theme})
</button>
);
}
// Auth context
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const userData = await response.json();
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// App with providers
function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
</AuthProvider>
);
}useRef
import { useRef, useEffect, useState } from 'react';
// DOM reference
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
// Store mutable value
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (!intervalRef.current) {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => stop(); // Cleanup
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
// Previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}useMemo dan useCallback
import { useState, useMemo, useCallback } from 'react';
// useMemo - memoize expensive calculations
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// Only recalculate when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
// useCallback - memoize functions
function TodoList() {
const [todos, setTodos] = useState([]);
// Function reference stays the same
const addTodo = useCallback((text) => {
setTodos(prev => [
...prev,
{ id: Date.now(), text, completed: false }
]);
}, []);
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
}, []);
return (
<div>
<TodoForm onAdd={addTodo} />
<TodoItems todos={todos} onToggle={toggleTodo} />
</div>
);
}
// Child component won't re-render unnecessarily
const TodoForm = React.memo(({ onAdd }) => {
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
onAdd(input);
setInput('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
});Custom Hooks
// useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
setValue(newValue);
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.error(error);
}
};
return [value, setStoredValue];
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
// useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchComponent() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
useEffect(() => {
if (debouncedSearch) {
// API call here
console.log('Searching for:', debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
);
}Forms
Controlled Components
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
remember: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const validate = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Submit form
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<div>
<label>
<input
name="remember"
type="checkbox"
checked={formData.remember}
onChange={handleChange}
/>
Remember me
</label>
</div>
<button type="submit">Login</button>
</form>
);
}Styling
CSS Modules
// Button.module.css
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background-color: #007bff;
color: white;
}
.secondary {
background-color: #6c757d;
color: white;
}
// Button.jsx
import styles from './Button.module.css';
function Button({ variant = 'primary', children, ...props }) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
{...props}
>
{children}
</button>
);
}Styled Components
import styled from 'styled-components';
const Button = styled.button`
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
color: white;
&:hover {
opacity: 0.8;
}
`;
const Card = styled.div`
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: ${props => props.theme.cardBg};
`;
function App() {
return (
<Card>
<h1>Hello</h1>
<Button primary>Primary</Button>
<Button>Secondary</Button>
</Card>
);
}Performance Optimization
React.memo
// Prevent unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
console.log('Rendering ExpensiveComponent');
return <div>{data}</div>;
});
// Custom comparison
const UserCard = React.memo(
({ user }) => {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.user.id === nextProps.user.id;
}
);Code Splitting
import { lazy, Suspense } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}Latihan Praktis
Project 1: Todo App
Buat todo application dengan:
- Add, edit, delete todos
- Mark as complete
- Filter (all, active, completed)
- Local storage persistence
- Responsive design
Project 2: Weather App
Buat weather application dengan:
- Fetch data dari API
- Search by city
- Display current weather
- 5-day forecast
- Loading dan error states
Project 3: E-commerce Cart
Buat shopping cart dengan:
- Add/remove items
- Update quantities
- Calculate totals
- Context API untuk state
- Local storage
Kesimpulan
React adalah tool yang powerful untuk membangun modern web applications. Dengan menguasai components, hooks, dan best practices, Anda dapat membangun aplikasi yang interactive, performant, dan maintainable.