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.
Why is ReactJS essential for developers?
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.
JSX and functional components
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>
);
}
Key notes:
- JSX looks like HTML, but it is JavaScript; transpiled to actual JavaScript using a transpiler like Babel.
- A component must always be capitalized.
- A component should return a single JSX element ( which can have as many children as it wants ). If you don’t want to wrap inside an element, you can use the fragment
<></>
.
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
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} />
);
}
State
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>
);
}
Key notes:
- State updates are asynchronous, so we should never mutate the state directly. Instead, we should always use the setter function.
-
useState() hook returns an array with two values:
- Value: The current value of the state.
- Setter function: A function to update the state.
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);
Event handling
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.
Conditional rendering
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;
}
Listing and keys
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 }
];
Lifting the state up
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.
Handling component lifecycle
A React component goes through 3 lifecycle stages:
- Mounting: When the component is to be rendered into the DOM tree.
- Update: When any of the props or the state changes.
- Unmount: When the component is about to be removed from the DOM tree.
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>
);
}
useEffect patterns:
- useEffect(() => {}): Runs after every render.
- useEffect(() => {}, []): Runs only once (on mount).
- useEffect(() => {}, [dependency]): Runs when dependency changes
- useEffect(() => { return () => {} }, []): The returned function is invoked when the component is about to unmount.
Referencing a DOM element
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() hook
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>
)}
</>
);
}
Use cases of the useLayoutEffect() hook:
- Measuring DOM elements.
- Synchronizing animations.
- Preventing visual flickers.
- Updating scroll positions.
useId() hook
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>
);
}
Custom hook
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>
);
}
Characteristics of a custom hook
- A custom/normal hook cannot be called conditionally.
- A custom/normal hook can only be used within a React component.
- A hook should be called at the top to avoid conditionally rendering it.
Error boundaries
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
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.
Lazy loading and suspense
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>
);
}
Conclusion
ReactJS remains a cornerstone of modern web development. By mastering these concepts, you’ll be equipped to build robust, scalable applications.
Key notes:
- Always use the functional components as they are easy to use and recommended by the React team.
- Always use a dependency array in the useEffect() to monitor the side effects to avoid infinite loops.
- Keep components small and have them as a singular unit with different variations, like a button with different colors.
- Never mutate the state directly; always use the setter function.
- Use descriptive names to define the components, variables, functions, etc, to make their purpose easy to understand.
- Always be prepared for the worst and handle the error states effectively.
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.
Existing customers can download the new version of Essential Studio® on the license and download page. If you are not a Syncfusion® customer, try our 30-day free trial to check out our incredible features.
If you have any questions, contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!
Related Blogs
- RxJS for React: Unlocking Reactive States
- Master Asynchronous JavaScript with RxJS
- What’s New in Next.js 15 RC?
- What’s New in React 19?
This article was originally published at Syncfusion.com.
Top comments (0)