React has revolutionized frontend development with its component-based architecture, enabling developers to build dynamic, interactive UIs efficiently. At the heart of React's power are three core concepts: props (for passing data), state (for managing internal data), and conditional rendering (for controlling what gets displayed based on conditions). Mastering these isn't just about writing code, it's about creating maintainable, performant apps that scale.
In this comprehensive guide, we'll explore each concept in depth, with step-by-step explanations, code examples, and best practices. Whether you're building a simple todo app or a complex enterprise dashboard, these fundamentals will elevate your React skills. I'll draw from my experience optimizing React apps for high-traffic sites to provide practical insights. Let's dive in, prerequisites: Basic JavaScript and React setup (e.g., via Create React App).
Props (short for "properties") are the mechanism for passing data from a parent component to its children. They're immutable, read-only, and act like function arguments, making components reusable and composable.
Props allow you to customize child components without hardcoding values. For instance, in a real-world e-commerce app, you might pass product details as props to a reusable ProductCard component. This promotes the "single responsibility principle," where components focus on rendering rather than data fetching.
Key characteristics:
Pro Tip: Always validate props with PropTypes or TypeScript to catch errors early, I've seen untyped props cause subtle bugs in large teams.
Let's create a simple Greeting component that receives a name prop.
Step 1: Define the child component:
// Greeting.js
import React from 'react';
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
export default Greeting;
Step 2: Pass props from a parent:
// App.js
import React from 'react';
import Greeting from './Greeting';
function App() {
return <Greeting name="Alice" />;
}
export default App;
This renders "Hello, Alice!". For multiple props, use object destructuring:
function Greeting({ name, age }) {
return <h1>Hello, {name}! You are {age} years old.</h1>;
}
Default Props: Set fallbacks with defaultProps.
Greeting.defaultProps = { name: 'Guest' };
Prop Functions: Pass callbacks for child-to-parent communication (e.g., handling form submissions). Common Pitfall: Avoid mutating props directly, clone objects if needed to prevent side effects.
In my projects, I've used props to build configurable UI libraries, reducing code duplication across apps.
State represents the internal, mutable data of a component that can change over time, triggering re-renders. Unlike props, state is managed within the component and is ideal for user interactions, form data, or fetched API responses.
State is initialized in class components with this.state or in functional components with the useState hook (introduced in React 16.8). When state updates, React efficiently re-renders only the affected parts of the DOM via its reconciliation algorithm.
Why use state?
useMemo or useCallback for optimizations.Pro Tip: For global state (e.g., user auth across an app), consider Context API or Redux; I've migrated apps from local state to Redux for better scalability.
Functional components with hooks are the modern standard. Here's a counter example:
Step 1: Import and initialize state:
// Counter.js
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initial state: 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
Step 2: Update state: setCount is asynchronous; for updates based on previous state, use a callback: setCount(prev => prev + 1).
For older codebases:
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>
);
}
}
Common Pitfall: Don't update state directly (e.g., this.state.count++); always use setState to ensure React detects changes.
In production, I've used state for features like real-time form validation, combining it with effects (useEffect) for side effects like API calls.
Conditional rendering lets you show or hide elements based on state, props, or other logic—essential for dynamic interfaces like loading spinners or user roles.
React doesn't have built-in conditionals like if in JSX, so we use JavaScript expressions:
This keeps JSX clean and declarative.
Step 1: Simple Toggle with State:
function ToggleButton() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
{isVisible && <p>This is visible!</p>}
</div>
);
}
Step 2: Ternary for Alternatives:
function UserStatus({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>}
</div>
);
}
Here, isLoggedIn could be a prop or state.
Step 3: Complex Conditions with Variables:
function WeatherDisplay({ temperature }) {
let message;
if (temperature > 30) {
message = <p>It's hot! ☀️</p>;
} else if (temperature < 10) {
message = <p>It's cold! ❄️</p>;
} else {
message = <p>It's mild. 🌤️</p>;
}
return <div>{message}</div>;
}
Pro Tip: For performance in lists, use keys with conditional elements to help React's diffing algorithm. Avoid overusing conditionals, extract to subcomponents for readability.
In my experience, conditional rendering shines in dashboards, where I've hidden admin panels based on user roles fetched via state.
To master these concepts, let's build a TodoList app that integrates all three.
// TodoList.js
import React, { useState } from 'react';
function TodoItem({ todo, onToggle }) { // Props: todo object and callback
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
<button onClick={() => onToggle(todo.id)}>Toggle</button>
</li>
);
}
function TodoList() {
const [todos, setTodos] = useState([]); // State: array of todos
const [input, setInput] = useState(''); // State: form input
const addTodo = () => {
if (input) {
setTodos([...todos, { id: Date.now(), text: input, completed: false }]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.length > 0 ? (
todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
))
) : (
<p>No todos yet! Add one above.</p> // Conditional rendering
)}
</ul>
</div>
);
}
export default TodoList;
This example passes todo as props to TodoItem, manages todos with state, and uses conditionals to show a message if the list is empty. It's a solid foundation for more features like persistence with localStorage.
Mastering props, state, and conditional rendering unlocks React's full potential, allowing you to build intuitive, efficient UIs. Practice by extending the todo app, add features like editing or filtering.