DEV Community

Cover image for ReactJS Essentials Every Developer Should Know
Zahra Sandra Nasaka for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

ReactJS Essentials Every Developer Should Know

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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}!</>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 }
];
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

This article was originally published at Syncfusion.com.

Top comments (0)