Skip to content

*React Unfolded

A comprehensive guide to React fundamentals, hooks, and patterns.

19 min read

What is React?

When React was introduced by Facebook in 2013, it was nothing short of revolutionary. It gave developers the power to build highly dynamic and interactive user interfaces using components and a virtual DOM, which drastically improved performance for large-scale, state-driven applications.

The developer experience with React was (and still is) smooth, declarative, and expressive. It quickly became one of the most widely adopted libraries in the JavaScript ecosystem, powering platforms like Instagram, Facebook, Airbnb, and many more.

  • React is a JavaScript library for building user interfaces.
  • React is used to build single-page applications.
  • React allows us to create reusable UI components.
  • React creates a VIRTUAL DOM in memory.
    • Instead of manipulating the browser's DOM directly, React creates a virtual DOM in memory, where it does all the necessary manipulating, before making the changes in the browser DOM.
  • React's goal is in many ways to render HTML in a web page.
  • React renders HTML to the web page by using a function called createRoot() and its method render().

Virtual DOM (Document Object Model)

  • It's nothing but the replica of the original DOM.
  • After any changes in the virtual DOM, first the vdom will compare the changes with the odom and then only that changes will be updated and not the whole DOM.

React Lifecycle Methods

You can work with them using the useEffect hook.

It's like a cycle from birth to death:

  • When your component first gets created it will get attached to the DOM and that is called componentDidMount → by writing inside the useEffect.
  • After the completion of the work of the component you can remove them from the DOM and that method is called componentWillUnmount → by returning from the useEffect.

React JSX

  • JSX allows you to write HTML tags inside the JavaScript code.
  • The HTML code must be wrapped in ONE top level element.
    • You can use a "fragment" (<></>) to wrap multiple lines.
  • When JSX is rendered, it translates className attributes into class attributes.
  • There are only 3 rules for JSX:
    1. 1Return a single root element
    2. 2Close all the tags
    3. 3camelCase most of the things!

Expressions in JSX

Write expressions inside curly braces { }.

const myElement = <h1>React is {5 + 5} times better with JSX</h1>;

Multiple Lines in JSX

Use parentheses:

const myElement = (
  <ul>
    <li>Apples</li>
    <li>Bananas</li>
    <li>Cherries</li>
  </ul>
);

Conditions — if Statements

React supports if statements, but not inside JSX.

const x = 5;
let text = "Goodbye";
if (x < 10) {
  text = "Hello";
}
const myElement = <h1>{text}</h1>;

Use a ternary expression inside JSX:

const x = 5;
const myElement = <h1>{x < 10 ? "Hello" : "Goodbye"}</h1>;

React Components

  • When creating a React component, the component's name MUST start with an upper case letter.
  • Two types: Class components and Function components.
    • In older React code bases, you may find Class components primarily used. It is now suggested to use Function components along with Hooks.

Function Component

function Car() {
  return <h2>Hi, I am a Car!</h2>;
}

Rendering a Component

<Car />

Props (Properties)

Props are like function arguments, and you send them into the component as attributes.

function Car(props) {
  return <h2>I am a {props.color} Car!</h2>;
}

<Car color="red" />

Components in Components

We can refer to components inside other components:

function Car() {
  return <h2>I am a Car!</h2>;
}

function Garage() {
  return (
    <>
      <h1>Who lives in my Garage?</h1>
      <Car />
    </>
  );
}

Components in Files

// file1.js
function Car() {
  return <h2>Hi, I am a Car!</h2>;
}
export default Car;

// file2.js
import Car from './Car.js';

function Garage() {
  return (
    <>
      <h1>Who lives in my Garage?</h1>
      <Car />
    </>
  );
}

React Props

  • Props are arguments passed into React components.
  • Props are passed to components via HTML attributes.
function Car(props) {
  return <h2>I am a {props.brand}!</h2>;
}

function Garage() {
  const carName = "Ford";
  return (
    <>
      <h1>Who lives in my garage?</h1>
      <Car brand={carName} />
    </>
  );
}

React Events

  • React has the same events as HTML: click, change, mouseover, etc.
  • React events are written in camelCase syntax: onClick instead of onclick.
  • React event handlers are written inside curly braces: onClick={shoot}.
function Football() {
  const shoot = () => {
    alert("Great Shot!");
  };
  return <button onClick={shoot}>Take the shot!</button>;
}

Passing Arguments

Send value as a parameter to the function, using arrow function:

function Football() {
  const shoot = (a) => {
    alert(a);
  };
  return <button onClick={() => shoot("Goal!")}>Take the shot!</button>;
}

React Event Object

Event handlers have access to the React event that triggered the function.

function Football() {
  const shoot = (a) => {
    alert(a.type);
  };

  return <button onClick={(event) => shoot(event)}>Take the shot!</button>;
}

React Conditional Rendering

  • if Statement
  • Logical && Operator
  • Ternary Operator

1. if Statement

Two functions will render conditionally:

function MissedGoal() {
  return <h1>MISSED!</h1>;
}

function MadeGoal() {
  return <h1>Goal!</h1>;
}

function Goal(props) {
  const isGoal = props.isGoal;
  if (isGoal) {
    return <MadeGoal />;
  }
  return <MissedGoal />;
}

// MissedGoal will run
<Goal isGoal={false} />

2. Logical && Operator

If cars have length only then render:

function Garage(props) {
  const cars = props.cars;
  return (
    <>
      <h1>Garage</h1>
      {cars.length > 0 && <h2>You have {cars.length} cars in your garage.</h2>}
    </>
  );
}

const cars = ["Ford", "BMW", "Audi"];
<Garage cars={cars} />

3. Ternary Operator

You can directly use ternary operator inside the return:

function Goal(props) {
  const isGoal = props.isGoal;
  return (
    <>
      {isGoal ? <MadeGoal /> : <MissedGoal />}
    </>
  );
}

React Lists

  • The JavaScript map() array method is generally the preferred method to render lists.

Keys

Keys allow React to keep track of elements. This way, if an item is updated or removed, only that item will be re-rendered instead of the entire list. Generally, the key should be a unique ID assigned to each item. As a last resort, you can use the array index as a key.

function Car(props) {
  return <li>I am a {props.brand}</li>;
}

function Garage() {
  const cars = [
    { id: 1, brand: "Ford" },
    { id: 2, brand: "BMW" },
    { id: 3, brand: "Audi" },
  ];
  return (
    <>
      <h1>Who lives in my garage?</h1>
      <ul>
        {cars.map((car) => (
          <Car key={car.id} brand={car.brand} />
        ))}
      </ul>
    </>
  );
}

React Forms

  • In HTML, form data is usually handled by the DOM.
  • In React, form data is usually handled by the components.
  • When the data is handled by the components, all the data is stored in the component state.
  • You can control changes by adding event handlers in the onChange attribute.

1. Multiple Input Fields

function MyForm() {
  const [inputs, setInputs] = useState({});

  const handleChange = (event) => {
    const name = event.target.name;
    const value = event.target.value;
    setInputs((values) => ({ ...values, [name]: value }));
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(inputs);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your name:
        <input
          type="text"
          name="username"
          value={inputs.username || ""}
          onChange={handleChange}
        />
      </label>
      <label>
        Enter your age:
        <input
          type="number"
          name="age"
          value={inputs.age || ""}
          onChange={handleChange}
        />
      </label>
      <input type="submit" />
    </form>
  );
}

2. Textarea

function MyForm() {
  const [textarea, setTextarea] = useState(
    "The content of a textarea goes in the value attribute"
  );

  const handleChange = (event) => {
    setTextarea(event.target.value);
  };

  return (
    <form>
      <textarea value={textarea} onChange={handleChange} />
    </form>
  );
}

3. Select

function MyForm() {
  const [myCar, setMyCar] = useState("Volvo");
  const handleChange = (event) => {
    setMyCar(event.target.value);
  };
  return (
    <form>
      <select value={myCar} onChange={handleChange}>
        <option value="Ford">Ford</option>
        <option value="Volvo" selected>Volvo</option>
        <option value="Fiat">Fiat</option>
      </select>
    </form>
  );
}

React Router

React Router is the most popular solution for page routing.

1. Main Page

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import Home from "./pages/Home";
import Blogs from "./pages/Blogs";
import Contact from "./pages/Contact";
import NoPage from "./pages/NoPage";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="blogs" element={<Blogs />} />
          <Route path="contact" element={<Contact />} />
          <Route path="*" element={<NoPage />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

2. Pages / Components

The Layout component has <Outlet> and <Link> elements. The <Outlet> renders the current route selected. <Link> is used to set the URL and keep track of browsing history. Anytime we link to an internal path, we will use <Link> instead of <a href="">.

import { Outlet, Link } from "react-router-dom";

const Layout = () => {
  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/blogs">Blogs</Link>
          </li>
          <li>
            <Link to="/contact">Contact</Link>
          </li>
        </ul>
      </nav>

      <Outlet />
    </>
  );
};

export default Layout;

React Memo

Using memo will cause React to skip rendering a component if its props have not changed. This can improve performance.

// index.js
import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState(["todo 1", "todo 2"]);

  const increment = () => {
    setCount((c) => c + 1);
  };

  return (
    <>
      <Todos todos={todos} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};
// Todos.js
import { memo } from "react";

const Todos = ({ todos }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
    </>
  );
};

export default memo(Todos);

Styling React Using CSS

There are many ways to use CSS in React. You can also create and import CSS modules. Use Tailwind for it.

Inline Styling

CamelCased Property Names: Use backgroundColor instead of background-color.

const Header = () => {
  return (
    <>
      <h1 style={{ backgroundColor: "lightblue" }}>Hello Style!</h1>
      <p>Add a little style!</p>
    </>
  );
};

JavaScript Object

Create a style object named myStyle:

const Header = () => {
  const myStyle = {
    color: "white",
    backgroundColor: "DodgerBlue",
    padding: "10px",
    fontFamily: "Sans-Serif",
  };
  return (
    <>
      <h1 style={myStyle}>Hello Style!</h1>
      <p>Add a little style!</p>
    </>
  );
};

React Hooks

Hooks allow us to "hook" into React features such as state and lifecycle methods. You must import Hooks from react.

3 rules for hooks:

  1. 1Hooks can only be called inside React function components.
  2. 2Hooks can only be called at the top level of a component.
  3. 3Hooks cannot be conditional.

React useState Hook

The React useState Hook allows us to track state in a function component.

  • Initialize: const [color, setColor] = useState("red");
  • Read: The value of color is "red".
  • Update: Use the state updater function. Never directly update state (e.g., color = "red" is not allowed).
import { useState } from "react";

function FavoriteColor() {
  const [color, setColor] = useState("red");

  return (
    <>
      <h1>My favorite color is {color}!</h1>
      <button type="button" onClick={() => setColor("blue")}>
        Blue
      </button>
    </>
  );
}

Updating Objects and Arrays in State

When state is updated, the entire state gets overwritten. Use the JavaScript spread operator to update nested properties.

function Car() {
  const [car, setCar] = useState({
    brand: "Ford",
    model: "Mustang",
    year: "1964",
    color: "red",
  });

  const updateColor = () => {
    setCar((previousState) => {
      return { ...previousState, color: "blue" };
    });
  };

  return (
    <>
      <h1>My {car.brand}</h1>
      <p>
        It is a {car.color} {car.model} from {car.year}.
      </p>
      <button type="button" onClick={updateColor}>
        Blue
      </button>
    </>
  );
}

React useEffect Hook

The useEffect Hook allows you to perform side effects in your components. Some examples of side effects are: fetching data, directly updating the DOM, and timers. useEffect accepts two arguments. The second argument is optional.

useEffect(() => {}, [dependency]);

Three dependency scenarios:

  1. 1No dependency passed: Runs on every render.
  2. 2An empty array: Runs only on the first render.
  3. 3Props or state values: Runs on the first render and any time any dependency value changes.
import { useState, useEffect } from "react";

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);

  return <h1>I have rendered {count} times!</h1>;
}

Effect Cleanup

Some effects require cleanup to reduce memory leaks. Timeouts, subscriptions, event listeners, and other effects that are no longer needed should be disposed. We do this by including a return function at the end of the useEffect Hook.

import { useState, useEffect } from "react";

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    let timer = setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);

    return () => clearTimeout(timer);
  }, []);

  return <h1>I've rendered {count} times!</h1>;
}

React useContext Hook

React Context is a way to manage state globally. It can be used together with the useState Hook to share state between deeply nested components more easily than with useState alone.

The Problem

State should be held by the highest parent component in the stack that requires access to the state. Without Context, you need to pass state as "props" through each nested component — this is called "prop drilling".

The Solution

Create context. Use the Context Provider to wrap the tree of components that need the state. All components in this tree will have access to the user Context.

import { useState, createContext } from "react";

const UserContext = createContext();

function Component1() {
  const [user, setUser] = useState("Jesse Hall");

  return (
    <UserContext.Provider value={user}>
      <h1>{`Hello ${user}!`}</h1>
      <Component2 user={user} />
    </UserContext.Provider>
  );
}
import { useContext } from "react";

function Component5() {
  const user = useContext(UserContext);

  return (
    <>
      <h1>Component 5</h1>
      <h2>{`Hello ${user} again!`}</h2>
    </>
  );
}

React useRef Hook

The useRef Hook allows you to persist values between renders. It can be used to store a mutable value that does not cause a re-render when updated. It can be used to access a DOM element directly.

1. Does Not Cause Re-renders

import { useState, useEffect, useRef } from "react";

function App() {
  const [inputValue, setInputValue] = useState("");
  const count = useRef(0);

  useEffect(() => {
    count.current = count.current + 1;
  });

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h1>Render Count: {count.current}</h1>
    </>
  );
}

2. Accessing DOM Elements

import { useRef } from "react";

function App() {
  const inputElement = useRef();

  const focusInput = () => {
    inputElement.current.focus();
  };

  return (
    <>
      <input type="text" ref={inputElement} />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

3. Tracking State Changes

import { useState, useEffect, useRef } from "react";

function App() {
  const [inputValue, setInputValue] = useState("");
  const previousInputValue = useRef("");

  useEffect(() => {
    previousInputValue.current = inputValue;
  }, [inputValue]);

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h2>Current Value: {inputValue}</h2>
      <h2>Previous Value: {previousInputValue.current}</h2>
    </>
  );
}

React useReducer Hook

The useReducer Hook is similar to the useState Hook. It is useful for managing complex state logic in functional components.

useReducer accepts two arguments: useReducer(<reducer>, <initialState>). The reducer function contains your custom state logic and the initialState can be a simple value but generally will contain an object. It returns the current state and a dispatch method.

import { useReducer } from "react";

const initialTodos = [
  { id: 1, title: "Todo 1", complete: false },
  { id: 2, title: "Todo 2", complete: false },
];

const reducer = (state, action) => {
  switch (action.type) {
    case "COMPLETE":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: !todo.complete };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

function Todos() {
  const [todos, dispatch] = useReducer(reducer, initialTodos);

  const handleComplete = (todo) => {
    dispatch({ type: "COMPLETE", id: todo.id });
  };

  return (
    <>
      {todos.map((todo) => (
        <div key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleComplete(todo)}
            />
            {todo.title}
          </label>
        </div>
      ))}
    </>
  );
}

React useCallback Hook

The React useCallback Hook returns a memoized callback function. Think of memoization as caching a value so that it does not need to be recalculated. This allows us to isolate resource intensive functions so that they will not automatically run on every render. The useCallback Hook only runs when one of its dependencies update.

// main component
import { useState, useCallback } from "react";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };

  const addTodo = useCallback(() => {
    setTodos((t) => [...t, "New Todo"]);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};
// Todo component
import { memo } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("child render");
  return (
    <>
      <h2>My Todos</h2>
      {todos.map((todo, index) => {
        return <p key={index}>{todo}</p>;
      })}
      <button onClick={addTodo}>Add Todo</button>
    </>
  );
};

export default memo(Todos);

Every time a component re-renders, its functions get recreated. Because of this, the addTodo function has actually changed — and the addTodo prop change causes memo to not prevent re-rendering. useCallback fixes this by memoizing the function.

React useMemo Hook

The useCallback and useMemo Hooks are similar. The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.

import { useState, useMemo } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };

  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

React Custom Hooks

Hooks are reusable functions. When you have component logic that needs to be used by multiple components, we can extract that logic to a custom Hook. Custom Hooks start with "use". Example: useFetch.

// useFetch.js
import { useState, useEffect } from "react";

const useFetch = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data));
  }, [url]);

  return [data];
};

export default useFetch;
// index.js
import useFetch from "./useFetch";

const Home = () => {
  const [data] = useFetch("https://jsonplaceholder.typicode.com/todos");

  return (
    <>
      {data &&
        data.map((item) => {
          return <p key={item.id}>{item.title}</p>;
        })}
    </>
  );
};

React Limitations: SEO and Performance

While React apps offer great interactivity, they do so by rendering content on the client-side. This means that when a user (or a web crawler) visits a React-powered site, the browser initially receives:

<html>
  <head>
    <!-- Meta tags, stylesheets -->
  </head>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

What's missing? The actual page content.

For a search engine crawler (like Googlebot or DuckDuckBot), this is problematic. These crawlers ideally need to see your actual content — headings, paragraphs, links, metadata — right away to index and rank the page properly.

Common issues with React in traditional setups:

  1. 1

    No Server-Side Rendering (SSR) by default React renders the page content in the browser after loading JavaScript. By the time content appears, crawlers may have already skipped or misinterpreted the page.

  2. 2

    Poor SEO performance Since crawlers often don't wait for JavaScript to fully execute, your React app might not display any meaningful content to them, leading to low search rankings.

  3. 3

    Excessive Re-rendering If not carefully architected, React components can re-render too frequently, causing performance issues, jankiness, or memory leaks — especially in large applications.

What Crawlers See in a Basic React App

<html>
  <head>
    <title>My Website</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/static/js/main.js"></script>
  </body>
</html>

There's no meaningful content for the crawler to index — only a <div id="root">, waiting for JavaScript to render the rest of the app. This results in:

  • Lower rankings in search engines
  • Poor previews in social media sharing
  • Weak initial performance for users on slow networks

Solutions and Best Practices

To overcome these limitations, developers turned to enhanced frameworks or returned to simpler stacks for specific use cases.

1. Write in Plain HTML, CSS, and JavaScript

For simple static websites, such as blogs, documentation, or portfolios, sticking to the basics ensures full SEO support and fast load times.

Pros:

  • Full control
  • Zero client-side dependencies
  • SEO-friendly
  • Blazingly fast performance

2. Use Next.js — React with SSR and SSG Built-In

Next.js is a powerful React framework built by Vercel that solves many of React's shortcomings:

  • Server-Side Rendering (SSR)
  • Static Site Generation (SSG)
  • Built-in routing
  • File-based structure
  • Automatic code splitting

It combines the flexibility of React with the performance and SEO-friendliness of server-rendered pages.

3. Use Astro — Best for Static Sites

Astro is a modern static site builder designed for content-heavy websites. It allows you to write in multiple frameworks (React, Vue, Svelte, etc.) but renders the site as static HTML with zero JavaScript by default.

Ideal for:

  • Blogs
  • Marketing websites
  • Documentation

Astro delivers the best of both worlds: SEO-optimized static output, while still letting you opt-in to JavaScript for interactivity when needed.

Conclusion

React is still a powerful tool, but it's not a one-size-fits-all solution — especially when it comes to SEO and performance. Understanding its limitations and pairing it with the right architecture or framework is key to building fast, scalable, and discoverable websites.

Whether you go with:

  • Plain HTML/CSS/JS for simplicity and speed,
  • Next.js for dynamic but SEO-friendly React apps, or
  • Astro for blazing-fast static sites,

...you'll be making a more informed and strategic choice depending on your project's needs.

April 20, 2025wtfpulkit.dev