TL;DR: Navigating the evolving ReactJS landscape? This guide breaks down essential ReactJS concepts, including hooks, state management, JSX, and emerging patterns like React Server Components. With practical code examples, you’ll learn how to build efficient, scalable applications faster.
ReactJS is the most popular JavaScript framework for creating the frontend of web applications. You can use it to create single-page web applications, mobile applications with React-Native, Server-side rendered applications with NextJS, and more.
ReactJS continues to dominate frontend development. Whether you’re building dashboards, mobile apps, or enterprise-grade UIs, understanding its core concepts is non-negotiable. This guide breaks down the essentials.
The smallest UI element of the DOM can be converted to a component in React. Components are the smallest building unit that accepts props and returns JSX while still being flexible enough to maintain its state. Different components can be composed together to create a new component or module.
JSX stands for JavaScript XML, a syntax available in React that helps to use JavaScript functions as HTML elements.
// Functional Component
function Welcome({ name }) {
return (
<h1>Hello, {name}!</h1>
);
}
// Using the component
function App() {
return (
<div>
<Welcome name="Sarah" />
<Welcome name="John" />
</div>
);
} <></>. function Welcome({ name }) {
return <>Hello, {name}!</>;
} We can wrap multiple children using fragments without an extra DOM node.
function UserInfo({ user }) {
return (
<>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.role}</p>
</>
);
}
// Functional Component for Blog Post using React.Fragment
function BlogPost({ post }) {
return (
<React.Fragment key={post.id}>
<h1>{post.title}</h1>
<p>{post.content}</p>
</React.Fragment>
);
} Props, short for properties, are the arguments passed to React Components. They are the readable values and can change the component’s behavior with different values.
Props are passed from the parent to the child component, making the components highly flexible and reusable. Using props, we can alter the component with different variations.
function UserCard({ user, isOnline }) {
return (
<div className="user-card">
<img src={user.thumbnail} alt={user.name} />
<h3>{user.name}</h3>
<span className={isOnline ? 'online' : 'offline'}>
{isOnline ? 'Online' : 'Offline'}
</span>
</div>
);
}
// Usage
function App() {
const user = { name: 'Alice', thumbnail: '/dp.jpg' };
return (
<UserCard user={user} isOnline={true} />
);
} The state helps the component store and manage the data. In React, we cannot use variables as each component is a function, and on rendering, the variable will be re-declared; thus, we have special hooks that we use for different purposes.
The useState() hook stores the data, which triggers the component’s re-rendering when its value changes.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
} The setter function also accepts a callback function, which allows us to access the previous value. This helps to keep the mutation synchronous.
setCount((count) => count + 1); React uses JSX, a sugar coat around JavaScript that allows us to write JavaScript as HTML. This does not just reduce the developer learning curve; it also helps to streamline things.
For example, React uses synthetic events, which help us assign the same events to different HTML elements.
You can listen to events like clicks, form submissions, and keyboard input by directly assigning them to the elements.
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault(); // Prevent page refresh
console.log('Login attempt:', { email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
} This makes React easier to work with, especially with Form elements, as we don’t have to listen to different events on different form elements.
We can conditionally render the components with different logic blocks and operators; ultimately, everything is JavaScript.
function Dashboard({ user }) {
return (
<div>
{user ? (
<div>
<h1>Welcome back, {user.name}!</h1>
<UserProfile user={user} />
</div>
) : (
<div>
<h1>Please log in</h1>
<LoginForm />
</div>
)}
{/* Using && for conditional rendering */}
{user?.isAdmin && <AdminPanel />}
</div>
);
} You can also return null from the component, apart from JSX, if you want to render nothing.
function Dashboard({ user, isLoading }) {
if (isLoading) {
return <p>...loading</p>;
}
if (user) {
return <UserProfile user={user} />;
}
return null;
} JSX can be rendered as an array of items that will render it as a list, and keys are used as a unique identifier among similar siblings, which helps React in reconciliation.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span>{todo.text}</span>
<button onClick={() => toggleTodo(todo.id)}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
</li>
))}
</ul>
);
}
// Example data
const todos = [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build a project', completed: false },
{ id: 3, text: 'Get a job', completed: false }
]; If two sibling components want to share the data, they can do that through the common parent component.
function App() {
const [temperature, setTemperature] = useState('');
return (
<div>
<TemperatureInput
temperature={temperature}
onTemperatureChange={setTemperature}
/>
<BoilingVerdict celsius={parseFloat(temperature)} />
</div>
);
}
function TemperatureInput({ temperature, onTemperatureChange }) {
return (
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
placeholder="Enter temperature in Celsius"
/>
);
}
function BoilingVerdict({ celsius }) {
if (celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
} React components can receive functions as props, which they can then invoke from themselves. Thus, the parent component can pass a callback function to the child and do a mutation when invoked.
A React component goes through 3 lifecycle stages:
React provides an inbuilt hook called useEffect() that is called for all these 3 lifecycle events. We can then use it to handle the side effects, such as making the API call when the component mounts, doing the DOM manipulation, assigning the event listeners, memory clean up on unmount, etc.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after every render
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Dependency array - runs when userId changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
} There will often be a scenario where you want to reference the actual DOM node, assign the event listeners, or perform other operations.
For that, React provides an inbuilt hook called useRef(), which can be used to reference the DOM nodes.
import { useRef, useEffect } from 'react';
function App({ user, isLoading }) {
const btnRef = useRef();
useEffect(() => {
buttonRef?.current?.addEventListener('click', (e) => {
console.log('Button clicked');
});
// Memory clean up
return () => {
buttonRef?.current?.removeEventListener('click', () => {});
};
}, []);
return <button ref={btnRef}>clear</button>;
} useRef() serves another purpose: storing the values that should not trigger re-renders. It can be an alternative to useState() as a variable to store values.
useLayoutEffect() is similar to the useEffect() hook, but it runs synchronously after the DOM mutations, before the browser paints; thus, it can be used to make the visual changes to the DOM before it is visible to the user.
import { useState, useLayoutEffect, useRef } from 'react';
function TooltipComponent({ children, tooltipText }) {
const [tooltipStyle, setTooltipStyle] = useState({});
const elementRef = useRef(null);
const tooltipRef = useRef(null);
useLayoutEffect(() => {
if (elementRef.current && tooltipRef.current) {
const elementRect = elementRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// Calculate position to avoid overflow
const left = elementRect.left + (elementRect.width - tooltipRect.width) / 2;
const top = elementRect.top - tooltipRect.height - 8;
setTooltipStyle({
position: 'fixed',
left: Math.max(8, left),
top: Math.max(8, top),
zIndex: 1000
});
}
}, [tooltipText]);
return (
<>
<div ref={elementRef}>
{children}
</div>
{tooltipText && (
<div ref={tooltipRef} style={tooltipStyle} className="tooltip">
{tooltipText}
</div>
)}
</>
);
} The useId() hook can generate unique IDs, which can be used as keys if you don’t have a unique identifier, or for the accessibility attributes.
import { useId, useState } from 'react';
function AccessibleForm() {
const nameId = useId();
const emailId = useId();
const errorId = useId();
const [formData, setFormData] = useState({
name: '',
email: '',
description: ''
});
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = {};
if (!formData.name) newErrors.name = 'Name is required';
if (!formData.email) newErrors.email = 'Email is required';
setErrors(newErrors);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor={nameId}>Name *</label>
<input
id={nameId}
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
aria-describedby={errors.name ? `${nameId}-error` : undefined}
aria-invalid={!!errors.name}
/>
{errors.name && (
<div id={`${nameId}-error`} role="alert" className="error">
{errors.name}
</div>
)}
</div>
<div>
<label htmlFor={emailId}>Email *</label>
<input
id={emailId}
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
aria-describedby={errors.email ? `${emailId}-error` : undefined}
aria-invalid={!!errors.email}
/>
{errors.email && (
<div id={`${emailId}-error`} role="alert" className="error">
{errors.email}
</div>
)}
</div>
<button type="submit">Submit</button>
</form>
);
} Any reusable logic that may or may not involve using built-in hooks can be extracted to a custom function whose name starts with use, maintaining the clear separation of concern.
// Custom hook for fetching data
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
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>
);
} A React component can be isolated into an error boundary that wraps it, which will help in the cascading failure if the component results in an error.
In case of error, we can display fallback UI.
import { Component } from 'react';
// Class component for Error Boundary (Hooks don't support error boundaries yet)
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error details
console.error('Error caught by boundary:', error, errorInfo);
this.setState({
error,
errorInfo
});
// You can also log to the error reporting service here
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Oops! Something went wrong</h2>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Component that might throw an error
function BuggyComponent({ shouldThrow }) {
if (shouldThrow) {
throw new Error('I crashed!');
}
return <div>Everything is working fine!</div>;
}
// Usage
function App() {
const [shouldThrow, setShouldThrow] = useState(false);
return (
<div>
<button onClick={() => setShouldThrow(!shouldThrow)}>
{shouldThrow ? 'Fix Component' : 'Break Component'}
</button>
<ErrorBoundary>
<BuggyComponent shouldThrow={shouldThrow} />
</ErrorBoundary>
</div>
);
} Portals in React can be used to render the component outside the parent DOM hierarchy. This is useful when you want to render only one component, irrespective of where it is invoked from, like Modals, Tooltips, and Overlays.
import { useState } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.body // Render directly to document.body
);
}
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Modal Title</h2>
<p>This modal is rendered outside the component tree!</p>
<button onClick={() => addToast('Modal action completed!', 'info')}>
Action Button
</button>
</Modal>
</div>
);
} You can pass the reference of any other DOM element or use the normal JavaScript selectors to select the DOM element.
We can lazy-load the components and show a fallback UI using suspense. By excluding the current component from the main JS bundle, we enable tree-shaking and keep the bundle lightweight.
import { Suspense, lazy, useState } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
// Loading component
function LoadingSpinner() {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
function App() {
const [currentPage, setCurrentPage] = useState('dashboard');
const renderPage = () => {
switch (currentPage) {
case 'dashboard':
return <Dashboard />;
case 'profile':
return <Profile />;
case 'settings':
return <Settings />;
default:
return <Dashboard />;
}
};
return (
<div>
<nav>
<button
onClick={() => setCurrentPage('dashboard')}
className={currentPage === 'dashboard' ? 'active' : ''}
>
Dashboard
</button>
<button
onClick={() => setCurrentPage('profile')}
className={currentPage === 'profile' ? 'active' : ''}
>
Profile
</button>
<button
onClick={() => setCurrentPage('settings')}
className={currentPage === 'settings' ? 'active' : ''}
>
Settings
</button>
</nav>
<main>
<Suspense fallback={<LoadingSpinner />}>
{renderPage()}
</Suspense>
</main>
</div>
);
} ReactJS remains a cornerstone of modern web development. By mastering these concepts, you’ll be equipped to build robust, scalable applications.
These concepts are the foundations of React; mastering them will help you to create an enterprise-grade, scalable web application. Want to accelerate your development? Explore Syncfusion’s React UI components for ready-to-use, enterprise-grade solutions.
If you have any questions, contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!