React Fundamentals

Flashcard 1/15
What is React and why would you use it over other libraries or frameworks?

All 15 React Fundamentals flashcards

Every question in this free section, with its full answer — open one to check yourself, or use the card view above to drill them in order.

What is React and why would you use it over other libraries or frameworks?

React is a JavaScript library for building user interfaces. It uses a component-based architecture and a virtual DOM to efficiently update the UI. Created by Facebook, it has become a popular choice for building web applications due to its performance, large ecosystem, and ease of creating reusable UI components.

Compared to other libraries or frameworks, React often excels in flexibility, a large developer community, and a wide range of third-party packages. It's also widely supported and easy to integrate with other tools and frameworks, making it a go-to choice for many developers.

Because React focuses solely on the view layer, it can be combined with various state management solutions and routing libraries. This allows developers to build highly customized solutions without being locked into a monolithic architecture.

How do props differ from state in React?

Props (short for properties) are used to pass data from a parent component down to child components. They are read-only and cannot be modified by the child receiving them. State, on the other hand, is data that is managed within a component. It's mutable and typically used to track data that changes over time or in response to user actions.

// Example: illustrating props vs state
function ChildComponent({ message }) {
  return <p>{message}</p>;
}

function ParentComponent() {
  const [counter, setCounter] = React.useState(0);

  return (
    <div>
      {/* 'message' is passed as props to the ChildComponent */}
      <ChildComponent message="Hello from Parent!" />

      {/* 'counter' is state managed by ParentComponent */}
      <p>Count: {counter}</p>
      <button onClick={() => setCounter(counter + 1)}>
        Increment Counter
      </button>
    </div>
  );
}

In this example, message is a prop passed from the parent to the child, while counter is local state in the parent. The child component cannot change the message prop, but the parent can update its own counter state with setCounter.

What are functional components in React, and how are they different from class components?

Functional components are JavaScript functions that accept props and return JSX. They are simple, lightweight, and rely on React Hooks for state and lifecycle features. Class components, on the other hand, are ES6 classes that extend React.Component and utilize lifecycle methods for managing state and side effects.

// Example of a Class Component
class GreetingClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    console.log('Class component mounted');
  }

  render() {
    return (
      <div>
        <p>Hello, {this.props.name}!</p>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increase
        </button>
      </div>
    );
  }
}

// Example of a Functional Component
function GreetingFunction({ name }) {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    console.log('Functional component mounted');
  }, []);

  return (
    <div>
      <p>Hello, {name}!</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
}

Class components were once the standard for managing state and lifecycle. However, with the introduction of Hooks, most new development favors functional components for simpler syntax and better code reuse. Hooks provide direct APIs for state (useState), side effects (useEffect), context (useContext), and more.

Can you explain controlled vs. uncontrolled components when handling form inputs?

Controlled components are form inputs that derive their current value from React state. Every update to the input is handled by React, making the form data the single source of truth. Uncontrolled components, on the other hand, rely on the DOM to maintain their state. You typically access their values through refs instead of React state.

// Controlled Component
function ControlledInput() {
  const [value, setValue] = React.useState("");

  return (
    <div>
      <label>Controlled:</label>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <p>Current value: {value}</p>
    </div>
  );
}

// Uncontrolled Component
function UncontrolledInput() {
  const inputRef = React.useRef(null);

  const handleSubmit = () => {
    alert(`Uncontrolled value: ${inputRef.current.value}`);
  };

  return (
    <div>
      <label>Uncontrolled:</label>
      <input type="text" ref={inputRef} />
      <button onClick={handleSubmit}>Show Value</button>
    </div>
  );
}

// In controlled components, React state is the single source of truth.
// In uncontrolled components, the DOM keeps track of the input's value.

Controlled components offer more predictable behavior and make it easier to implement features like validation, but they can be more verbose to set up. Uncontrolled components require less code but can be harder to manage when the application grows in complexity.

What is the Virtual DOM, and how does it improve performance in React?

The Virtual DOM is an in-memory representation of the real DOM. React uses it to determine the minimal set of changes needed to update the UI efficiently. Rather than updating the browser DOM directly on every change, React first recalculates the UI in the virtual representation, then applies only the necessary operations to the real DOM.

This improves performance because DOM manipulations are relatively expensive. By batching and minimizing DOM updates, React can render complex interfaces more efficiently than if it directly manipulated the real DOM each time the data changes.

In practice, you rarely deal with the Virtual DOM directly. React’s declarative model handles the abstraction. As long as you manage state correctly, React will handle reconciling the differences between virtual and real DOM behind the scenes.

Why are keys important in React when rendering lists or iterating over data?

In React, a key is a special prop that helps identify which items in a list have changed, been added, or removed. The key prop should be a stable, unique identifier for each list item (often an ID).

// Example demonstrating the importance of "keys"
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        // The "key" prop helps React identify each list item
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Without proper key props, React’s reconciliation algorithm can lead to incorrect or inefficient updates, since React cannot correctly track the position and identity of list items. Using a stable key makes your application perform better and behave more predictably.

What is the purpose of the useState Hook, and can you demonstrate a basic usage example?

The useState Hook is a way to manage state in functional components. It lets you add local state to a component, replacing the need for this.state in class components. You call useState with an initial value, and it returns the current state value and a function to update it.

function Counter() {
  // Declare a new state variable "count" with a default value of 0
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

In this example, count holds the current state, and setCount updates it. Each time the button is clicked, the component re-renders with the new state, updating the UI to show the latest count value.

How does the useEffect Hook differ from lifecycle methods in class components, and when would you use each approach?

Class components use lifecycle methods (e.g., componentDidMount, componentDidUpdate, componentWillUnmount) to handle side effects. In functional components, the useEffect Hook consolidates this logic into a single API. You pass a function to useEffect that runs after each render by default, and you can control when it runs with a dependency array.

// Example: using useEffect to fetch data on component mount
function DataFetchingComponent() {
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    async function fetchData() {
      const response = await fetch("https://api.example.com/items");
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []); // Empty dependency array means this effect runs once

  return (
    <ul>
      {data.map((item) => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

With useEffect, you can handle tasks like data fetching, subscriptions, or manually updating the DOM. Its cleanup function works similarly to componentWillUnmount. You’d generally prefer functional components with useEffect in modern codebases because they provide a simpler, more unified way to handle side effects compared to multiple lifecycle methods in classes.

When would you use the Context API, and how does it help manage state across your application?

The Context API in React provides a way to share data across multiple components without having to pass props manually at every level (sometimes called 'prop drilling'). It's useful for application-wide data such as theme settings, user information, or localization data.

// Example: Creating and using a ThemeContext
const ThemeContext = React.createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const { theme, setTheme } = React.useContext(ThemeContext);
  return (
    <button
      style={{ background: theme === "light" ? "#fff" : "#333" }}
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      Current theme: {theme}
    </button>
  );
}

Here, the ThemeProvider makes the theme state available to any components wrapped by it. Consuming components like ThemedButton can call React.useContext to access or update the theme. This avoids passing theme as props through every intermediate component.

What are some best practices for conditional rendering in React? Could you provide a quick example or two?

In React, conditional rendering determines which UI elements appear based on certain conditions (e.g., a piece of state, a prop value). Common patterns include using JavaScript logical operators (like &&) or the ternary operator (conditional ? outcome 1 : outcome 2) to conditionally return different JSX.

// Example 1: Using short-circuit && operator
function Welcome({ isLoggedIn, userName }) {
  return (
    <div>
      <h1>Welcome!</h1>
      {isLoggedIn && <p>Hello, {userName}!</p>}
    </div>
  );
}

// Example 2: Using ternary operator
function StatusMessage({ status }) {
  return (
    <div>
      {status === "loading" ? (
        <p>Loading...</p>
      ) : status === "success" ? (
        <p>Data loaded successfully!</p>
      ) : (
        <p>Error loading data.</p>
      )}
    </div>
  );
}

Use clear and readable expressions when conditionally rendering. Complex conditions can be extracted into helper functions or variables. Avoid deeply nested conditionals in JSX, as it can hurt readability.

How do error boundaries work in React, and can you show a simple code example of creating one?

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log the errors, and display a fallback UI instead of the component tree that crashed. They were introduced to handle runtime errors gracefully in production.

// Example: Simple ErrorBoundary class component
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render shows the fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // You can log the error to an error reporting service
    console.error('Error Boundary caught an error', error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong.</h2>;
    }
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

Notice that ErrorBoundary is a class component. Currently, error boundaries only work with class components. If any of the children throw an error during rendering, lifecycle methods, or constructors, the ErrorBoundary will catch it, set its state, and render a fallback UI.

What strategies would you use to optimize a React application’s performance?

Some common performance optimization strategies in React include memoization with React.memo, useMemo, and useCallback; code splitting with dynamic imports (e.g., React.lazy and Suspense); and efficient state management (avoiding unnecessary re-renders).

// Example: using React.memo to prevent re-renders
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  // heavy computation or complex UI
  return <div>{data.value}</div>;
});

// Example: using useMemo for expensive calculations
function Stats({ numbers }) {
  const total = React.useMemo(() => {
    console.log("Calculating total...");
    return numbers.reduce((sum, num) => sum + num, 0);
  }, [numbers]);

  return <p>Total: {total}</p>;
}

You can also minimize expensive DOM operations by keeping component trees small and properly using key props in lists. In production, leveraging tools like React Profiler helps identify bottlenecks. Always measure performance before and after changes to ensure improvements are effective.

Can you describe how React’s reconciliation process (the “Fiber” architecture) updates the UI?

React Fiber is the internal engine behind React’s reconciliation process. It breaks down the rendering work into small units and processes them in a prioritized manner (e.g., handling urgent updates like user input before less critical work). This allows React to pause and resume rendering, improving responsiveness in complex applications.

During reconciliation, React compares the Virtual DOM tree from the previous render with the new one. The Fiber architecture assigns ‘fibers’ to each node, allowing React to track changes and efficiently determine which parts of the UI need to be updated.

This approach helps React handle animations, gestures, and interactions smoothly. In older React versions (pre-Fiber), rendering could be blocked by expensive updates. With Fiber, React can process high-priority tasks first and postpone or split up lower-priority tasks until later.

How do you approach data fetching in React, and what are some common patterns or Hooks you might use (e.g., useEffect, react-query, etc.)?

In React, a common pattern is to fetch data inside a useEffect Hook. This ensures the data is fetched after the component mounts. You can also use libraries like react-query (TanStack Query) for a more robust solution, providing caching, revalidation, and other advanced features.

// Example: Basic data fetching with useEffect
function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    async function fetchData() {
      const res = await fetch("/api/users");
      const data = await res.json();
      setUsers(data);
    }
    fetchData();
  }, []);

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
// Example: Using react-query (TanStack Query)
import { useQuery } from '@tanstack/react-query';

function UserList() {
  const { data: users = [], isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch("/api/users");
      return res.json();
    },
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error fetching data.</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Using useEffect is straightforward, but can lead to repetitive boilerplate code. Libraries like react-query abstract common concerns like caching and refetching, making data fetching logic more maintainable and efficient.

What is server-side rendering (SSR) and how does Next.js integrate with React to support SSR?

Server-side rendering (SSR) refers to rendering React components on the server and sending fully formed HTML to the client. This can improve performance for initial page loads and enhance SEO, since search engines can easily crawl rendered HTML.

Next.js is a React framework that provides built-in support for SSR and static site generation (SSG). It lets you write React components, and then handle data fetching and routing on the server side. This means your app can render pages at request time using server data, or build them at build time for faster subsequent loads.

// Example (for Next.js 13 with the App Router):
// app/users/page.js

// This is a server component by default, which can fetch data at request time.
export default async function UsersPage() {
  const res = await fetch('https://api.example.com/users');
  const users = await res.json();

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

In older versions of Next.js (pre-13) with the Pages Router, getServerSideProps was used to fetch data on the server at request time. In the App Router, components can be marked as server or client components, allowing you to easily fetch and render data on the server or handle interactivity on the client.