CSCI 4513
Week 11, Lecture 19
Three gods each gave distinct gifts to transform lifeless wood into living humans, just as state, interactivity, and JSX combine to transform static HTML into dynamic React applications.
The Tale:
In the early days after the gods had shaped land, sea, and sky, Odin, Vili, and Vé walked along a beach and discovered two tree trunks washed ashore—an ash tree (Ask) and an elm tree (Embla). These were lifeless pieces of wood, beautiful in form but inanimate and empty.
The three gods saw potential and decided to transform them into the first humans. Odin gave them önd—breath, spirit, and life force. Vili gave them óðr and lá—consciousness, the ability to move, and willpower. Vé gave them lá, lúr, and heill—appearance, senses, and facial features. None of these gifts alone would have been sufficient. Breath without consciousness is merely animation. Consciousness without form has no way to interact with the world. Form without life is just another tree trunk. But together, these three gifts transformed lifeless wood into living, thinking, perceiving humans who could experience and shape their world.
// Static HTML - lifeless tree trunk
<div class="user-card">
<h2>User Name</h2>
<p>Status: Active</p>
</div>
// Odin's gift - breath/life through state
function UserCard() {
const [user, setUser] = useState({
name: 'User Name',
status: 'Active'
});
// Component can now "breathe" - data changes
}
// Vili's gift - consciousness and action
const handleClick = () => {
setUser(prev => ({...prev, status: 'Inactive'}));
};
useEffect(() => {
// Component can act autonomously
}, []);
// Vé's gift - appearance and perception
return (
<div className="user-card">
<h2>{user.name}</h2>
<button onClick={handleClick}>Toggle</button>
</div>
);
What is a Component?
A component is a piece of UI (user interface) that has its own logic and appearance.
Components can be as small as a button, or as large as an entire page.
Key Concept: React applications are built by composing components together.
// A simple React component
function Welcome() {
return <h1>Hello, World!</h1>;
}
// Components are just JavaScript functions!
// They return JSX (which looks like HTML)
Requirements:
React uses capitalization to distinguish between HTML tags and components.
// ❌ Wrong - lowercase (React thinks it's HTML)
function welcome() {
return <h1>Hello!</h1>;
}
// ✅ Correct - capitalized
function Welcome() {
return <h1>Hello!</h1>;
}
// Default export (one per file)
export default function Welcome() {
return <h1>Hello!</h1>;
}
// Named export (multiple per file)
export function Welcome() {
return <h1>Hello!</h1>;
}
export function Goodbye() {
return <h1>See you later!</h1>;
}
// Importing a default export
import Welcome from './Welcome';
// Importing named exports
import { Welcome, Goodbye } from './Greetings';
// Importing both
import Welcome, { Goodbye } from './Greetings';
// Using the imported component
function App() {
return (
<div>
<Welcome />
<Goodbye />
</div>
);
}
JSX is a syntax extension for JavaScript
It lets you write HTML-like markup inside JavaScript files
// JSX looks like HTML...
const element = <h1>Hello, world!</h1>;
// But it's actually JavaScript!
// Babel converts it to:
const element = React.createElement(
'h1',
null,
'Hello, world!'
);
Wrap multiple elements in a parent (or use fragments)
Self-closing tags need the slash: <img />, <br />
class → className, onclick → onClick
// ❌ Wrong - multiple root elements
function App() {
return (
<h1>Hello</h1>
<p>Welcome!</p>
);
}
// ✅ Correct - wrapped in parent
function App() {
return (
<div>
<h1>Hello</h1>
<p>Welcome!</p>
</div>
);
}
// ✅ Also correct - using Fragment
function App() {
return (
<>
<h1>Hello</h1>
<p>Welcome!</p>
</>
);
}
// ❌ Wrong - unclosed tags
<img src="photo.jpg">
<br>
<input type="text">
// ✅ Correct - all tags closed
<img src="photo.jpg" />
<br />
<input type="text" />
// Regular tags also need closing
<div>
<p>Content</p>
</div>
// HTML attribute → JSX attribute
class → className
for → htmlFor
onclick → onClick
onchange → onChange
tabindex → tabIndex
stroke-width → strokeWidth
// Example:
<div class="container"> {/* HTML */}
<div className="container"> {/* JSX */}
<label for="name"> {/* HTML */}
<label htmlFor="name"> {/* JSX */}
// Use curly braces {} to embed JavaScript
function Greeting() {
const name = "Alice";
const time = new Date().toLocaleTimeString();
return (
<div>
<h1>Hello, {name}!</h1>
<p>The time is {time}</p>
<p>2 + 2 = {2 + 2}</p>
</div>
);
}
// Any valid JavaScript expression works!
// Variables, function calls, math, etc.
// HTML
<div class="card">
<img src="avatar.jpg">
<label for="name">Name:</label>
<input type="text" id="name">
</div>
// JSX
<div className="card">
<img src="avatar.jpg" />
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
</div>
Tip: Use an HTML to JSX converter for large blocks!
Search for "HTML to JSX converter" online
function FruitList() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{fruits.map(fruit => (
<li>{fruit}</li>
))}
</ul>
);
}
// Output:
// • Apple
// • Banana
// • Cherry
function UserList() {
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
return (
<div>
{users.map(user => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
</div>
))}
</div>
);
}
Keys help React identify which items have changed, been added, or removed
Without keys, React can't track list items efficiently
If you don't provide keys, React will use array indices by default.
This can cause bugs when the list order changes!
// ✅ Good - using unique IDs
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
users.map(user => (
<div key={user.id}>{user.name}</div>
));
// ❌ Bad - using array index (can cause bugs)
users.map((user, index) => (
<div key={index}>{user.name}</div>
));
// ❌ Worse - generating keys during render
users.map(user => (
<div key={Math.random()}>{user.name}</div>
));
// Using if/else
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
} else {
return <h1>Please sign in.</h1>;
}
}
// Using ternary operator
function Greeting({ isLoggedIn }) {
return (
<h1>
{isLoggedIn ? 'Welcome back!' : 'Please sign in.'}
</h1>
);
}
// Show element only if condition is true
function Notifications({ count }) {
return (
<div>
<h1>Inbox</h1>
{count > 0 && (
<p>You have {count} unread messages</p>
)}
</div>
);
}
// If count is 0, nothing renders
// If count > 0, the paragraph renders
Tip: Use && when you only want to show something if the condition is true
Props (short for "properties") are how you pass data from parent to child components
Think of props like function parameters - they let you customize a component
// Parent passes props
<Greeting name="Alice" age={25} />
// Child receives props
function Greeting(props) {
return <h1>Hello, {props.name}! Age: {props.age}</h1>;
}
// Without destructuring
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// With destructuring (preferred!)
function Greeting({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
</div>
);
}
// Usage is the same
<Greeting name="Alice" age={25} />
// Using default parameters
function Greeting({ name = "Guest", role = "User" }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>Role: {role}</p>
</div>
);
}
// If no props provided, defaults are used
<Greeting /> // Hello, Guest! Role: User
<Greeting name="Alice" /> // Hello, Alice! Role: User
<Greeting name="Bob" role="Admin" /> // Hello, Bob! Role: Admin
// Parent component
function App() {
const handleClick = (name) => {
alert(`Hello, ${name}!`);
};
return <Button onClick={handleClick} text="Click me" />;
}
// Child component
function Button({ onClick, text }) {
return (
<button onClick={() => onClick(text)}>
{text}
</button>
);
}
This is how child components communicate with parents!
State is data that changes over time
When state changes, React re-renders the component to reflect the new data
Key Difference:
Props are passed from parent (like function parameters)
State is managed within the component (like local variables)
import { useState } from 'react';
function Counter() {
// useState returns an array with 2 elements:
// 1. Current state value
// 2. Function to update the state
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
const [count, setCount] = useState(0);
// ^ ^ ^
// | | |
// state setter initial value
// Reading state
console.log(count); // 0
// Updating state
setCount(5); // count is now 5
setCount(count + 1); // count is now 6
State updates trigger a re-render of the component
Never modify state directly! Always use the setter function
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// What is count now? 3? No! It's 1!
};
return <button onClick={handleClick}>{count}</button>;
}
Why? State is a snapshot at render time
All three calls use the same count value (0)
So they all set count to 1
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ Wrong - uses snapshot value
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Result: count = 1
// ✅ Correct - uses updater function
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: count = 3
};
return <button onClick={handleClick}>{count}</button>;
}
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ Wrong - mutating state directly
user.age = 26; // Don't do this!
setUser(user); // React won't detect the change
// ✅ Correct - create new object
setUser({ ...user, age: 26 });
// For arrays
const [items, setItems] = useState([1, 2, 3]);
// ❌ Wrong
items.push(4);
// ✅ Correct
setItems([...items, 4]);
function Form() {
const [name, setName] = useState('');
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello, {name}!</p>
</div>
);
}
// The input is "controlled" because:
// 1. Its value comes from state
// 2. onChange updates the state
// React controls what's displayed
function RegistrationForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<form>
<input name="name" value={formData.name}
onChange={handleChange} />
<input name="email" value={formData.email}
onChange={handleChange} />
<input name="age" value={formData.age}
onChange={handleChange} />
</form>
);
}
1. Only call hooks at the top level
Don't call hooks inside loops, conditions, or nested functions
2. Only call hooks from React functions
Call hooks from functional components or custom hooks
function Component() {
// ✅ Correct - at top level
const [count, setCount] = useState(0);
// ❌ Wrong - inside condition
if (count > 0) {
const [name, setName] = useState('');
}
// ❌ Wrong - inside loop
for (let i = 0; i < 3; i++) {
const [num, setNum] = useState(i);
}
// ❌ Wrong - inside event handler
const handleClick = () => {
const [clicked, setClicked] = useState(false);
};
}
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
}
};
return (
<div>
<input value={input}
onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
📅 Next Class: React Router
📝 Homework: CV Application Project