Getting Started With React Without Losing Your Mind
React has a reputation for being overwhelming when you first approach it. The docs change, the tooling recommendations shift, and there is enough jargon to make anyone want to stick with jQuery for another decade. I spent about two years building production React apps before I felt competent enough to explain it to someone else without making things worse. This walkthrough covers the parts that actually matter in practice, not the theoretical framework the marketing team sells. Start by installing Node 18 or newer. Do not install Node 22 yet unless you enjoy debugging native module compatibility at 11pm on a Tuesday. Then run: npx create-react-app my-app
Yes, Create React App is officially deprecated. It still works fine for getting started. Vite is the current recommendation for new projects, but CRA gives you a more predictable file structure out of the box and less configuration magic hiding in config files you will never touch. Once the project boots, your folder structure should look roughly like this: node_modules, public, src, package.json, and src/index.js as your entry point. The src directory contains App.js, index.css, and a few other boilerplate files. Delete everything inside the return statement of App.js and write a simple JSX paragraph instead. Verify that it renders. This is the part where most people stall because they are reading documentation about components before they have actually seen one work on screen. Components are the core building block. A component is just a JavaScript function that returns JSX. That is it. Here is the simplest possible example:
function Button() { return <button>Click me</button> } JSX looks like HTML inside JavaScript. It compiles down to regular JavaScript function calls through Babel or SWC. If you think you need to understand the compilation pipeline before writing your first component, you are wasting time. Write the component first. Read about the compiler later if something breaks. Props pass data from parent to child. State lives inside a component. These two concepts handle ninety percent of what you need when you are starting out. The remaining ten percent is effect management, which trips people up constantly.
Get the Full Details

useState is how you manage state. Here is a realistic example of a counter component that you can actually use: import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ) } The important detail that beginners miss is that state updates are batched and asynchronous. Calling setCount three times in a row inside the same event handler will only trigger one re-render, and the final state value will be base plus three, not base plus one applied three separate times. Use the functional update form setCount(c => c + 1) if you depend on the previous value.
useEffect handles side effects. Data fetching, subscriptions, manual DOM manipulation. The dependency array controls when the effect runs. An empty array means run once on mount. No array means run after every render. Including variables means run when those variables change. This is straightforward until it is not. Here is a problem I ran into early that took me weeks to understand properly. I had a useEffect that fetched data when a userId prop changed. The effect fired correctly on mount, but when the parent component passed a new userId, the effect did not run because I had accidentally included the fetch call itself in the dependency array instead of just the userId. React either warns you about missing dependencies or silently skips the effect depending on your setup. My workaround was to move the fetch into its own named async function inside the component body and reference that function name only in the dependency array. The actual API call stayed stable across renders while the userId dependency drove the re-execution correctly. Context is how you share state across components without prop drilling. useContext combined with createContext gives you a global-ish state container. It works, but it re-renders every component that consumes it whenever any value in the context changes. I learned this the hard way on a dashboard application where a theme toggle was stored in context alongside user preferences. Every time someone changed their theme, every table component on the page re-rendered unnecessarily because they all consumed the same context object.
The fix was splitting the context. One context for theme, another for user data. This reduced re-renders significantly and made the code easier to reason about anyway. Splitting contexts is something the official docs barely mention. Custom hooks are where React starts to feel powerful instead of just verbose. A custom hook is a function that uses other hooks. It is not a React feature, it is a naming convention. You prefix it with use and it follows the same rules as any other hook. I wrote a useDebounce hook early in my career that looked like this: function useDebounce(value, delay) { const [debounced, setDebounced] = useState(value) useEffect(() => { const timer = setTimeout(() => setDebounced(value), delay) return () => clearTimeout(timer) }, [value, delay]) return debounced }

This pattern saves you from writing the same timeout cleanup logic in five different components. It is one of the most practical things you can extract into a reusable hook. Testing is where most beginner tutorials fail you. You learn the basics of components and hooks and then you hit a production codebase that has zero test coverage and no one knows how to test React properly. Testing Library is the standard approach now, not Enzyme. The philosophy is to test behavior, not implementation. Query by text, by role, by label. Assert that buttons exist and forms submit. Do not test that a component has exactly three divs inside it because that will break every time someone restructures the markup. Here is a realistic testing scenario I deal with regularly. A form component that validates an email field. The test should check that invalid input shows an error message and valid input allows submission. It should not check the internal state variable names or the exact CSS class applied to the error. Testing implementation details creates brittle tests that break during routine refactors.
Performance optimization is overhyped for beginners and understated for experienced engineers. Most React apps are fast enough without any manual optimization. useMemo and useCallback are not performance shortcuts you throw on everything. They are optimization tools you apply when profiling shows an actual bottleneck. I have seen junior developers add useCallback to every single function definition across an entire codebase. This adds complexity and maintenance burden without improving performance because React is already fast at calling regular JavaScript functions. When you actually need to optimize, the React DevTools Profiler is your best tool. It shows render costs, identifies why components re-render, and points you to the real problem instead of a guess. Spend twenty minutes learning to use it before spending two days wrapping things in useMemo. Routing is handled by react-router. The current major version uses hooks like useParams and useNavigate instead of wrapping everything in a router component. Here is a minimal setup that works:
import { BrowserRouter, Routes, Route } from 'react-router-dom' function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </BrowserRouter> ) } Keep routes near the top of your component tree. Do not nest BrowserRouter inside other components. Do this and you avoid a class of routing bugs that are very difficult to diagnose. State management libraries beyond Context include Zustand, Jotai, and Redux Toolkit. Zustand is the one I reach for most often now. It is minimal, does not require providers, and its API is straightforward. Redux Toolkit is still the right choice for large applications with complex state interactions and strict debugging requirements. The Redux DevTools extension is genuinely useful and worth the setup cost in those cases. For anything under two thousand lines of state logic, Context or Zustand is sufficient and does not add unnecessary complexity.

One specific workflow issue I encounter repeatedly is hot module replacement breaking unexpectedly. HMR works fine ninety-five percent of the time. The remaining five percent involves component state loss, broken route state, or weird import cycle errors that appear after a dependency update. The quickest fix is usually restarting the dev server entirely. Clearing .cache directories and node_modules won't help unless the issue is specifically about stale compiled files. Most HMR problems are transient. Building for production requires a build step. Vite handles this much faster than CRA ever did. The production build optimization is automatic and includes code splitting, minification, and tree shaking. You do not need to configure any of this manually for typical applications. Run the build command and verify the output folder contains static assets organized by route if you are using code-split lazy loading. Deployment is straightforward if you pick a platform that supports static hosting well. Vercel and Netlify both connect directly to GitHub and deploy on every push. Custom servers need Node.js, express, or a similar runtime if you are doing server-side rendering with Next.js. For a purely client-side React app, static hosting is all you need.
Common mistakes I see again and again. Mutating state directly instead of using the setter function. Putting API calls inside render without useEffect. Creating objects or arrays inside the component body that should be stable references. Not handling loading and error states in data-fetching components. These are not advanced topics. They are basic discipline issues that cause bugs throughout the entire development lifecycle. React itself does not solve everything. It is a view library. It does not handle routing, state management, or data fetching out of the box. This is intentional design. The ecosystem fills those gaps, but it also means you make more decisions than you would with a framework like Angular. Some people find this freedom exhausting. That is a reasonable opinion. If you are learning React, build something small and finish it. A task list, a weather app, a simple blog. Do not start with a clone of Netflix. Starting with a large project teaches you to avoid hard problems rather than learning fundamentals. Finishing a small project teaches you the full lifecycle from component design to deployment.
The official documentation at react.dev is actually good now. It replaced the old docs.reactjs.org site and focuses on modern patterns, hooks-first development, and practical examples. Read it in order. Skip ahead if you already know something, but do not skip the hooks section. The old class-based component documentation is still indexed in search results and it is outdated. That is the practical rundown. React is not simple, but it is not complicated either. The complexity comes from the ecosystem choices and the patterns you adopt over time, not from the core library itself. Write components. Manage state. Fetch data. Deploy. Repeat until it feels normal.
