How React Really Works (And What Next.js Adds on Top)

Hi, I am Adyasha Mohanty, a self taught developer extraordinaire from India. I am passionate about bringing ideas to life from the ground up whether that’s crafting beautiful or building intuitive user interfaces. Beyond coding, I thrive on sharing knowledge, engaging with the tech community and helping others grow. I love the entire journey of creation from the first line of code to shipping polished, user ready products. Let’s build, learn and ship amazing things together. 🚀
Intro ✨
Okay, full disclosure: I had no plans to write this. I was supposed to be working on a blog about my solo trip (which is still just a folder of photos, honestly) but instead, I ended up explaining React and Next.js to my cousin over the weekend. He was super curious about how stuff like useState and server-side rendering actually works. I grabbed a pen, sketched things out and it clicked for him. That moment that “aha!” made me think, hey, maybe this could help others too.
So this blog is more of a brain dump. If you’re someone who knows React but wants to understand why things work the way they do, this is for you.
What Happens When You Write React Code?
Let’s start with what you actually write:
function App() {
return <h1>Hello, World!</h1>;
}
It might feel like HTML inside JavaScript but this is actually JSX. And JSX isn’t something the browser understands directly, it gets converted by a compiler (like Babel) to a function call:
React.createElement('h1', null, 'Hello, World!');
That call creates a React element, not an actual DOM node basically just a plain JavaScript object that describes what you want on screen:
{
type: 'h1',
props: {
children: 'Hello, World!'
},
key: null,
ref: null
}
This is React's Virtual DOM, a lightweight, in memory representation of your UI. React holds onto this to later figure out what needs to be updated in the actual DOM.
What Actually Happens When a Component Renders?
Consider:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Here's what happens:
React renders the component, calling it like a function.
It sets up internal tracking via hooks by an internal "dispatcher".
When
useStateruns, it checks if it's the first render or a re-render and either initializes state or returns the latest.When you click the button,
setCounttriggers a re-render.React compares the new return value with the previous one and applies only the changed parts (we will talk about that in reconciliation).
Class Components:
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.increment}>{this.state.count}</button>;
}
}
Same idea, more boilerplate. Internally, React assigns an updater to manage setState, which schedules a re-render.
Class components give you lifecycle methods but they have largely been replaced by
useEffect,useLayoutEffectetc in modern apps.
Reconciliation: The Diff That Keeps Things Fast
This is where things get fun. Reconciliation is React’s algorithm for updating the UI efficiently.
Why does this matter?
DOM operations are expensive. So rather than refreshing the whole page (like jQuery used to do), React compares what’s changed and only touches those parts.
How it works:
A component’s state changes.
React re-renders that component and builds a new tree of React elements.
It compares this new tree with the old one.
React calculates the minimum number of operations to apply to the DOM.
Only the changed nodes are updated.
This process is called diffing and it’s fast O(n) because React uses some smart assumptions:
Elements with the same type get updated.
Keys help identify elements in a list.
If keys or types change, React assumes it’s a new element.
Real example:
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
With stable keys, React knows which list items were removed, moved or added.
This optimization is why React apps feel snappy.
The Rendering Pipeline in React
Here’s how rendering flows:
JSX turns into React elements
React creates or updates the Fiber tree
Fiber nodes map to real DOM nodes via
react-domDOM updates are batched and applied
React renders in two phases:
Render phase: pure, no DOM mutations
Commit phase: applies DOM changes, runs effects
The Fiber architecture enables React to pause and resume rendering, key to features like Suspense and Concurrent Mode.
Where Next.js Comes In
React doesn’t care where it renders. Browser, native, terminal? It just gives you the virtual DOM.
But when you want:
SEO
Server rendered pages
Static pages with dynamic data
...you need more. That’s where Next.js shines.
What Next.js Adds
Server-side rendering (SSR)
Static site generation (SSG)
File-based routing
Built-in API routes
Image optimization
It’s not just a framework — it’s an opinionated toolkit that works with React to make your life easier.
Traditional SPA (React only)
Server returns a mostly empty HTML shell
JS bundle loads
React mounts and fetches data
UI finally appears
With Next.js (SSR)
Server renders full HTML page with data
User sees meaningful content immediately
React hydrates it in the browser
What’s Hydration?
Let’s say you server render a page. The user gets full HTML: content, structure, everything.
But it’s not interactive yet.
Hydration is the process of attaching React’s logic i.e event listeners, hooks, state to that HTML. It turns your static HTML into a live React app in the browser without re-rendering the DOM.
This is possible because Next.js sends a JSON blob:
<script id="__NEXT_DATA__">{ props, paths, page info }</script>
This contains the props returned by getServerSideProps or getStaticProps, letting React "catch up" with what’s already on screen.
⚠️ If hydration fails (due to mismatch), React logs warnings and may re-render.
Things You Can't Do on the Server
Server rendering has limits. You are in a Node.js environment, not a browser. So:
No
window,document,localStorageNo user event handlers
No CSS media query logic
No DOM measurements (like
getBoundingClientRect())
If you need those, defer them to
useEffect()which only runs on the client.
Next.js Rendering Modes
| Mode | API Function | Runs On | Use Case |
| SSR | getServerSideProps | Every request | Personalised content |
| SSG | getStaticProps | At build time | Blogs, marketing pages |
| ISR | revalidate key in getStaticProps | On demand | News feeds, product updates |
This flexibility lets you choose performance or freshness depending on your page needs.
Final Thoughts
React gives you the tools to build UIs efficiently with declarative, component-driven code. It abstracts the pain of DOM manipulation and makes state management predictable.
But when you need a full-fledged web app with performance, SEO and server integration, you’ll want a framework like Next.js.
Next.js doesn’t replace React. It completes it.
Want to turn this into a series or go even deeper into Suspense, useTransition or how the React scheduler works? Let me know.
Also yes, that solo trip blog is still coming. Probably. Eventually :)



