A hundred days ago, I started a structured journey through React and Next.js with one goal: stop chasing features and start building better. Today, wrapping up this 100 days of React and Next.js: lessons series, I want to share what actually stuck—not the framework drama or the newest library, but the principles that made every project cleaner, faster, and more maintainable.
Less state, fewer bugs
Early in this journey, I was storing everything. Filter state. Derived calculations. UI toggles computed from other toggles. Every component felt like it needed its own useState calls. Then the bugs came: state out of sync, race conditions, inconsistent renders. The turning point was simple: I stopped asking "what data do I need?" and started asking "what data do I need to store?"
The difference is profound. Derived state is a bug factory. If you calculate it once and store it, you have two sources of truth. When one updates and the other doesn't, users see inconsistency. I learned to keep only what's truly state—user input, async progress, toggled UI—and derive everything else in render.
Here's a real example from a trading dashboard I built:
// Before: too much stored state
function PortfolioCard() {
const [holdings, setHoldings] = useState([]);
const [prices, setPrices] = useState({});
const [totalValue, setTotalValue] = useState(0);
const [totalGain, setTotalGain] = useState(0);
useEffect(() => {
// recalculate whenever holdings or prices change
setTotalValue(holdings.reduce((sum, h) => sum + h.quantity * prices[h.symbol], 0));
setTotalGain(holdings.reduce((sum, h) => sum + (prices[h.symbol] - h.cost) * h.quantity, 0));
}, [holdings, prices]);
}This pattern begets bugs. The effect runs late, dependencies are fragile, and if you forget a dependency, stale values persist. Now the same logic, minimal state:
// After: only core state, everything derived
function PortfolioCard() {
const [holdings, setHoldings] = useState([]);
const [prices, setPrices] = useState({});
const totalValue = useMemo(() =>
holdings.reduce((sum, h) => sum + h.quantity * prices[h.symbol], 0),
[holdings, prices]
);
const totalGain = useMemo(() =>
holdings.reduce((sum, h) => sum + (prices[h.symbol] - h.cost) * h.quantity, 0),
[holdings, prices]
);
}Or better yet, push it up as a selector in your state management layer. The point is: store the minimum, compute the rest. I watched entire classes of bugs disappear because there was only one source of truth.
The server is back
When I started this journey, Server Components were new. I was skeptical. Wasn't the whole point of React to own rendering? But after building features with them, I realized they're not replacing client React—they're doing what servers should have been doing all along.
A server component fetches data at build time or request time, eliminates a network roundtrip from the client, and sends HTML. That sounds simple until you remember every extra roundtrip is user-visible latency. On a ride-hailing app I worked on, moving coordinate queries to a Server Component cut time-to-interactive by 400ms. Users noticed.
The mental model shift matters: use Server Components for data fetching and static content, Client Components for interactivity. It's not revolutionary, but it's elegant.
// app/listings/[id]/page.tsx (Server Component)
async function ListingPage({ params }) {
const listing = await db.listings.findUnique({ id: params.id });
const reviews = await db.reviews.findMany({ listingId: params.id });
return (
<>
{/* This is a Client Component */}
);
}The booking widget handles state, event listeners, and user interaction. The parent fetches the data before anything renders. No loading states, no waterfall requests, no Suspense chains (unless you want them). The user gets content fast.
I've applied this pattern across e-commerce checkouts, SaaS dashboards, and travel booking flows. Every time, shifting data fetching to the server made the code simpler and the experience faster.
Performance is product
This is where theory meets reality. Early in my career, I optimized for code elegance. Fast sites were a nice-to-have. Then I saw the data: on one project, every 100ms improvement in page load correlated with a 2% increase in conversion. That's not a vanity metric—that's revenue.
But here's what surprised me: most performance wins aren't micro-optimizations. They're structural. Image optimization, lazy loading routes, Server Components, selective hydration—these are 80/20 wins. Profiling React renders with DevTools helped, but only after the architecture was right.
I started measuring real users, not lab conditions. Web Vitals matter: LCP (Largest Contentful Paint), FID (First Input Delay), CLS (Cumulative Layout Shift). Tools like Next.js Analytics and Vercel's built-in monitoring showed exactly where users waited.
// Dynamic imports for route-based code splitting
const AdminDashboard = dynamic(() => import('@/components/AdminDashboard'), {
loading: () => ,
ssr: false
});
export default function App() {
return (
);
}This pattern, applied across every heavy component, kept bundle sizes under control. Users on slow networks got interactive pages faster. I watched average session duration increase on projects after implementing route-based code splitting.
Fundamentals last
React Hooks replaced class components. The router changed twice. State management tooling cycles through trend after trend. But one thing hasn't changed in a decade: composition and data flow.
Understanding how data moves down and events bubble up is timeless. Knowing when to lift state, when to use context, and when to split components is more valuable than chasing the latest state management library. I've seen teams rewrite entire apps chasing trends while the real problem was messy data flow.
The frameworks will change. The next React might be signals instead of hooks. Next.js will add new rendering modes. But if you understand unidirectional data flow, component composition, and separation of concerns, you'll adapt to any framework in a week.
This is why I spent time early in the 100 days on the basics: closures, lifting state, context pitfalls, key prop behavior. Not because they're trendy—because they're foundational. A solid foundation makes every framework feel natural.
Common mistakes
- Optimizing before measuring: I've spent days perfecting memoization that saved 2ms in one component while ignoring a 3-second blocking image load. Always profile first.
- Over-fetching data: Requesting nested relationships you don't display or selecting columns you don't use adds latency on the wire. Be explicit about what you need.
- Derived state in useEffect: The reflex is to store computed values. Resist it. Compute in render unless there's a performance reason not to.
- Assuming Server Components solve everything: They're not a silver bullet. Complex interactive UIs still belong on the client. Use them strategically.
- Ignoring real user metrics: Your lab measurements don't match production. Implement analytics, check Web Vitals, and optimize what users actually experience.
Key takeaways
- Minimize stored state to eliminate entire classes of bugs. Derive what you can in render.
- Server Components aren't a return to pre-SPA patterns—they're a smarter split between server and client responsibilities.
- Performance is a feature, not an afterthought. Measure real users, not lab conditions, and prioritize structural improvements over micro-optimizations.
- Fundamentals compound. Data flow, composition, and state management principles are more durable than any framework feature.
- Build systems that are easy to understand and modify. Fancy code doesn't scale; maintainability does.
A hundred days isn't a mastery milestone—it's a checkpoint. I'll keep learning, and frameworks will keep evolving. But I'm confident now that I can pick up any React-based tool and ship solid products because I understand the principles underneath. That's what the last hundred days actually taught me.






Comments
Loading comments…