Starting a React Project Doesn't Have to Be Complicated
The most common mistake I see is people spending three hours setting up build tools before they write a single component. That is backwards. The actual work starts when you render something to the DOM, not when you configure Webpack or read about bundler caching strategies. I once spent an entire afternoon debugging a production issue where my custom Babel preset was stripping out React.createElement calls because I had configured it wrong inside a monorepo. That was 2019. We do not need that kind of pain anymore. The ecosystem has matured to the point where the default tooling is genuinely good enough for production apps. If you want a React Quick Start Guide With Examples, here is the practical version that reflects what actually works in real projects today.
Setting Up Your Environment
You need Node.js installed. That is the only hard requirement. Anything below version 18 will give you issues with modern packages. Check your version with node --version and upgrade if necessary. You can get it from nodejs.org. Then create a project using one of these commands:
npx create-react-app my-app
or
npm create vite@latest my-app -- --template react
I prefer Vite now. Create React App is still functional but its build times are slow and the development server uses webpack under the hood, which means longer reloads when you make changes. Vite uses esbuild and HMR that kicks in nearly instantly. For a typical React component file, you will see updates reflect in under 100 milliseconds on a standard laptop. Once your project is created, navigate into it and start the dev server:
Get the Full Details

cd my-app
npm run dev
That will launch a local server at localhost:5173. Open it in your browser. You will see the default React landing page. The project structure that comes with it includes a src folder containing an App.jsx file, an index.css file, and an HTML entry point. That is all you need to understand initially. React components are just JavaScript functions that return JSX, which looks like HTML but compiles down to JavaScript object trees. The browser does not read JSX directly. Babel transforms it during the build process. Here is a minimal component:
function Button() {
return <button className="btn">Click me</button>
}
The className attribute is used instead of class because "class" is a reserved keyword in JavaScript. This trips up beginners every time. There is no special trick to remember it. Just type className and move on. Components can accept data through props. Props are read-only objects passed from parent to child components:
function Greeting({ name }) {
return <p>Hello, {name}</p>
}
function App() {
return <Greeting name="Alex" />
}
When App renders, Greeting receives a props object with a name property set to "Alex". The curly braces inside JSX allow you to embed any JavaScript expression. This is how you make your components dynamic. Functional components use hooks to manage state and side effects. The most fundamental hook is useState: Each time the button is clicked, setCount triggers a re-render with the new value. The key thing to understand is that setState does not mutate state directly. It schedules an update. If you call setCount twice in the same function block without a re-render in between, both updates queue up.

This is where I ran into trouble in a project at a previous job. We had a form submission handler that called multiple setState calls sequentially and expected each one to be reflected in the next. It did not work because React batches state updates. The fix was to use the functional update form, passing a callback to setState instead of a direct value:
setCount(prevCount => prevCount + 1)
setCount(prevCount => prevCount + 1)
This guarantees each update reads the latest state value regardless of batching behavior. It took me about two weeks to figure out why my form wasn't computing the final total correctly. The issue was subtle enough that standard React documentation glosses over it. useEffect handles operations that happen outside the normal rendering cycle. Fetching data, subscribing to events, and manipulating the DOM are common examples: The second argument to useEffect is the dependency array. If you provide [userId], the effect re-runs whenever userId changes. If you omit it entirely, the effect runs after every render. If you pass an empty array [], it runs only once after the initial mount. I consistently see developers leave the dependency array empty when they should include values, which causes stale data bugs that are extremely difficult to trace later.
There is also a cleanup function you can return from useEffect. This is critical for preventing memory leaks:

useEffect(() => {
const subscription = eventBus.subscribe(handleEvent)
return () => subscription.unsubscribe()
}, [])
Without the cleanup function, the event listener persists even after the component unmounts. In large applications with many subscribers, this adds up quickly and can cause noticeable memory growth. Conditional rendering in React is straightforward. You can use ternary expressions, logical AND operators, or early returns: For lists, use the map method and always provide a unique key prop. The key helps React track which items have changed, been added, or removed:
Never use array indices as keys unless the list is completely static. When items are reordered or filtered, index-based keys cause rendering bugs where the wrong components update with stale data. I have spent entire sprints debugging issues that traced back to index keys in dynamic lists. Controlled components bind input values to state. Every keystroke triggers an onChange handler that updates the state, which then re-renders the component with the new value: Uncontrolled components are an alternative where React reads values from the DOM directly using refs. They are simpler for basic use cases but make validation and real-time feedback harder to implement. For most applications, controlled components are the better choice despite the extra code.
React has several gotchas that cause real problems in production. Here are the ones I encounter most often. Stale closures in async operations. When you capture a value inside an async function, that value is frozen at the time the closure was created. If your effect depends on a prop that changes, the async callback will still use the old value unless you explicitly pass it or restructure the logic. Object identity and re-renders. Creating objects or arrays inline in your component body, like const styles = { color: 'red' }, produces a new object reference on every render. If you pass this object as a prop to a memoized child component, that child will re-render unnecessarily because the reference changed even though the content is identical. The workaround is to define style objects outside the component or use useMemo.

Prop drilling is not always a problem. People talk about prop drilling as if it is a bug. It is not. It is a design choice. If you only need to pass data three or four levels deep, context is overkill and introduces complexity that hurts testability. Only reach for context when you genuinely need to share data across many unrelated components. React Query and similar libraries are worth learning early. Managing API state with useState and useEffect works for simple cases but breaks down as your application grows. You end up writing the same loading, error, and caching logic in multiple places. Libraries like TanStack Query handle this automatically and reduce the amount of custom state management code you need to write by roughly 60 percent in most projects I have worked on.
Building a Realistic Component from Scratch
Putting all of this together, here is a component you might actually use in production: This component handles its own state, performs debounced-ish searches through form submission, manages loading and error states, and maps results to a list with proper keys. It is not perfect. A production version would add debouncing to the input handler to avoid excessive API calls. But the core pattern is solid and scales well. The React ecosystem moves fast. Tools that were standard two years ago are being replaced by faster alternatives. The fundamentals—components, props, state, effects—have not changed significantly. Master those and you can adapt to whatever the next round of tooling improvements brings without starting over.