Intro to React + How I Organize Files

17 min read

Updated:

Introduction

When React was first released in 2013, it was touted as a library, not a framework. At that time, dominant web frameworks like AngularJS and Ember.js provided complete, “all-in-one” Model-View-Controller (MVC) architectures. They shipped with built-in tools for routing, form validation, HTTP data fetching, and dependency injection.

Since it is a library, there are many solutions and ways to do things, as it is unopinionated—unlike most relatives at family gatherings. Those opinionated relatives are like AngularJS and Ember.

The biggest problem React solves is keeping the UI synchronized with application state. This used to be a mammoth pain in the days of query selectors and vanilla JavaScript. Now, you’re the weird library on the block if you don’t have state management features.

In React, you describe what the screen should look like for a given state, and React figures out the DOM changes using Virtual DOM (VDOM). It sounds like really bad VR, but it’s actually very cool.

When state or props change, React creates an in-memory representation of the UI (the Virtual DOM), compares it to the previous version (a process called “diffing”), and updates only the necessary parts in the real browser DOM. This is a simplification of how modern React works internally, but it’s a useful mental model.

The React Team doesn’t really use this term anymore, so you may hear it referred to as a declarative UI runtime that relies on internal data structures like Fiber.

This post is not an exhaustive look at React, but rather, how I have come to enjoy organizing my projects in my many years of using the library.

The Mental Model of a React Component

To understand why I structure React projects the way I do, it helps to start with React’s mental model.

Imperative Code

Back when dinosaurs roamed the web, we used to write imperative UI code. This means we focused on writing step-by-step instructions to mutate the UI. This was buggy and error-prone. It also forced you to write a lot of sandwich code. For example:

// Prehistoric Imperative code
async function loadUserData() {
// First, we have to get our elements.
const profileContainer = document.getElementById('user-profile');
const spinner = document.getElementById('loading-spinner');
const errorMessage = document.getElementById('error-msg');
// --- Top Bread of the sandwich: Manually setup the "Loading" UI state
spinner.style.display = 'block';
errorMessage.style.display = 'none';
profileContainer.textContent = '';
try {
// --- The meat of what we actually want to do: Fetch the data
const response = await fetch('https://api.example.com/user');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
// --- Bottom of the sandwich (Success): Manually mutate DOM to show data
spinner.style.display = 'none';
profileContainer.innerHTML = `<h2>Welcome, ${data.name}</h2>`;
} catch (error) {
// --- Bottom of the sandwich (Failure): Manually mutate DOM to show error
spinner.style.display = 'none';
errorMessage.style.display = 'block';
errorMessage.textContent = 'Critical error: Could not load profile.';
}
}

The core problem with this code is state synchronization. The state of the application lives implicitly in the DOM. Some of the problems with this imperative code are that you could forget to clean up after yourself, it’s mentally taxing for the developer to follow, and if someone comes in and refactors the CSS, they could break the selector(s).

Declarative Code

React code is declarative. A component is just a function that takes props (think arguments or variables that may change the component) and return markup. For example:

function Greeting({ name }) {
return <p>Hello, {name}!</p>;
}
// <Greeting name="Kevin" />
// will always produce the same output, and React needs to only update
// the text node.

When props or state change, React reruns the function—in Strict mode, it runs it twice in a row to ensure it is pure (given the same input, it produces the same output). Contrary to popular belief, re-renders aren’t slow. It’s what re-rendering on the main thread does that makes things feel slow. If you have errant code that is causing lots of re-renders, it will sip CPU cycles, dropping frame rates for animations, creating a cludgy feeling when appending elements to the DOM, etc. The more errant code like this you have, the slower the application feels.

// The whole idea in eight lines
function Greeting({ name }) {
return <p>Hello, {name}!</p>;
}
// Given state `name = "Kevin"`, this is always what renders.
// Change name, the function runs again, React updates only the text node.

One thing that confused me early on was that return at the end of a React component. I wasn’t sure what I could put in there, but here’s a good way to think about it:

  • It just renders HTML
  • When you interpolate values into that HTML, you use the {} syntax.
  • Only use one-liner JavaScript within the return of a React Component/Hook:
return (someCondition
? items.map((i) => <Item key={item.id} name={item.name} />)
: <NoItems />
)

A very common way to render a list in React is to map data to a component. That key prop is a special one that identifies a component in a list.

Props and State

Both props and state should be thought of as immutable, because React often relies on reference equality (Object.is) when determining whether state values have changed and when optimizing re-renders.

In React, props are read-only properties—like attributes in HTML—that get passed to a component. This type of data can only flow down the component tree. When a prop changes, it will queue up a re-render of that component.

function MyComponent({ someProp, anotherProp }) {
//... Props flow downward. Think of them like reactive HTML attributes.
}

Reactive state is a component’s memory over time. Changing it triggers a re-render. There are a few different React hooks that manage state:

  • useState - A React hook (a special function) that returns a tuple, which is an immutable fixed sequence of elements. In this case, the first element in the sequence is the state, and the second is the function to set that state.
  • useReducer - A React hook that also returns a tuple. The first element is the state that was returned by the reducer function, and the second is a function that dispatches an action to that reducer.
function MyComponent() {
const [state, setState] = useState(initialState);
const [state, dispatch] = useReducer(reducer, initialArg, init);
// other code.
}

Early on, when I first started writing React code, I often used prop drilling—passing data down multiple layers of components to reach a deeply nested child. This can become a problem when data must pass through many layers of components that don’t use it directly; intermediate components become bloated with data they don’t use, breaking the principle of single responsibility.

One of the ways to handle this, if you need to reach a faraway child, is an airplane—I mean the Context API. Instead of using a bucket brigade, Context acts like a waterfall: you wrap a parent component to expose a value to an entire subtree without manually passing props through each layer, and any component or hook can reach in and grab exactly what it needs directly.

Once you understand how data moves through a React application, the next question becomes where all of that code should live.

How I Organize a React Project

Because React is a library, not a framework, it has no opinion about where your files go. This is freeing and paralyzing in equal measure. Over the years, I’ve landed on a structure that lets the next developer—often future me, who remembers nothing—reason about the project just by scanning folders. The whole philosophy boils down to one rule: organize things so that someone can guess where a file lives before they ever open the search bar.

Here’s the shape of it:

src
├── assets/ # css, images
├── components/ # React components and their styles
│ ├── App.tsx
│ └── DropDownMenu/
│ ├── DropDownMenu.tsx
│ └── DropDownMenu.css
├── hooks/ # useFetch.ts, useUser.ts
├── context/
│ └── User/
│ ├── UserContext.ts
│ └── UserProvider.tsx
├── reducers/
│ └── plotly-data/
│ ├── actions.ts
│ ├── reducers.ts
│ └── types.d.ts
├── util/ # pure functions: dates.ts, formatting.ts
├── constants/ # colors.ts, conversion.ts
├── errors/ # MainErrorBoundary.tsx
├── types.d.ts
└── main.tsx

A couple of structural habits I really like are:

  • First, src holds all the source code, and a sibling test folder mirrors its structure exactly—if you know where the component lives, you know where its test lives.
  • Second, the only .tsx files allowed at the top level src are your entry points. Everything else gets a folder. When the top level stays clean, finding main.tsx is trivial instead of a scavenger hunt.

A Folder for Each Kind of Thing

The top-level folders inside src aren’t arbitrary—each one maps to a kind of thing React knows about.

  • components holds your React components and any component-specific styles. If a component needs more than one file, give it a PascalCase folder named after itself: ./DropDownMenu/DropDownMenu.tsx, ./DropDownMenu/DropDownMenu.css. The folder is the component.
  • hooks holds your custom hooks. One useFetch.ts per concern.
  • context holds your Context—like the waterfall analogy from earlier. Use a folder named after the context (User), then prefix the files with that name: UserContext.ts and UserProvider.tsx. Splitting the context object from its provider keeps the “what” separate from the “how.”
  • util is for pure functions—same input, same output, same result. Group related functions into one file (dates.ts); resist the urge to make a file per function, or you’ll drown in import statements.
  • constants is for the values that never change. Same rule as util: group them, don’t make a lonely file for a single color.
  • errors is for your Error Boundaries, and every file ends in ErrorBoundary so there’s no ambiguity about what it does.

Types that require more than one file go in a top-level types.d.ts. The .d stands for declarations, and a nice trick is that TypeScript treats a declarations file with no imports or exports as global—the types are just there, no import required. When that file gets fat, promote it to a types/ folder using the same grouping rules as util and constants. Prefer using .d.ts files over importing types to reduce import clutter.

Dumb Components, Smart Hooks

You will save yourself a lot of pain and consternation if you follow this one simple rule: components should be dumb. A component’s job is to render markup. That’s it. The moment a component starts managing data fetching, transformations, and synchronization logic, it quickly becomes unmaintainable.

So I push all of that into custom hooks. So a component reads like a description of the screen, whereas a hook describes a single responsibility. You don’t need to extract a hook for every little thing—the React docs say some duplication is fine.

That said, you should really scrutinize your components as you write them to make sure they continue to read like a description of the screen. One of the ways things can get out of hand is by having too many useEffects in a component. When I first discovered the useEffect hook, I definitely abused it (e.g., hot potatoing state from one Effect to another, using it for console logs when state changes, things like that).

Since then, I think critically about whether I even need one in my components or custom hooks. When deciding, I ask myself three questions:

  1. “Do I need an Effect for this?” - Most of the time, you don’t need a useEffect. Props and state changing will cause a re-render to happen, which eliminates a whole category of why people reach for useEffect because you can write that logic in the component body or the Effect body. The next kind of use case is reacting to a state to set another state; this is a very slippery slope, more on that later. The idea is to scrutinize whether you absolutely need the Effect.
  2. “Does this make more sense in a named custom hook?” - It’s possible, if you’re thinking about adding an Effect, that you already have created some new state to track. If it’s not concerned with rendering, it might be good to lift all those pieces up and put them into a custom hook. For example, screen resizing with ResizeObserver, data fetching, data manipulation, API synchronization, authentication/authorization, etc. These are all examples of things that aren’t concerned with rendering.
  3. “Is this component still primarily concerned with rendering?” - This is an important one. You may have determined that your useEffect usage is simple enough that you don’t need to put it in a custom hook, and you’ve already determined it’s needed. So the real question is: does it distract from rendering? For example, maybe it’s simply an analytics call that gets fired on click. Simple enough, right? But what if you have to add 5 or 6 of them? For me, it comes down to how readable the component is after that: can I still easily tell what is being rendered.

The thing to remember here is that Effects run after React commits updates and exist specifically for synchronizing with external systems. This is intentional. They’re meant to be a side effect of rendering a component. The React documentation even says:

If you’re not trying to synchronize with some external system, you probably don’t need an Effect. — react.dev as of 06-02-2026

Think of it like being hungry and ordering DoorDash. Once your order is placed, that’s all asynchronous to you. You can go about your business without freezing in place until the driver rings your doorbell. This is a similar reason to use a useEffect when you want something external to happen. External may refer to the DOM, an API, localStorage, etc.

Here’s an example of a hook that uses ResizeObserver to determine the window dimensions. The component doesn’t need to tangle itself up in this logic; it can be lifted into a hook called useWindowDimension. Then the component can stay focused on rendering.

// The component stays focused on rendering.
function MyComponent() {
const { width, height } = useWindowDimensions();
// ...render something with width and height
}

The one anti-pattern I’ll call out by name: don’t hot-potato state between Effects. You know the move—one Effect sets a piece of state, which triggers a second Effect, which sets more state, which triggers a third. It feels reactive and clever. It is sadistic. It’s an abuse of reactivity, and when a bug in that chain of events happens, you will question the life choices that led you to become a software developer. I’ve experienced it many times!

And remember the rule that trips everyone up: hooks go at the top level, always, in the same order. React matches your hooks up by call order, so you can’t tuck one behind an if. Bail out after all your hooks have run, not before:

function MyComponent({ someProp }: MyComponentProps) {
const [state, setState] = useState();
useEffect(() => { /* ... */ }, []);
if (!someProp) return null; // ✅ after the hooks, not before
return <div>{/* ... */}</div>;
}

Naming Is Half the Battle

Consistent casing turns filenames into documentation. The convention I follow mirrors what you’ll see in the React docs themselves:

  • camelCase for hooks (useWindowDimensions.ts).
  • PascalCase for components, classes, context providers, and error boundaries.
  • kebab-case for everything else—plain JavaScript, images, non-component CSS. The casing alone tells you whether you’re looking at React-specific code or ordinary functional code.

I also prefer plain function declarations over arrow-function-assigned-to-const, and I’m consistent about exports: hooks get a named export, components get a default export.

// ✅ Hook: function + named export
export function useWindowDimensions() { /* ... */ }
// ✅ Component: function + default export
function MyComponent({ someProp }: MyComponentProps) { /* ... */ }
export default MyComponent;

Is this bikeshedding? A little. I generally prefer function declarations because I’ve been bitten by declaration-order issues more than once. With function declarations, JavaScript hoists the function definition, so the function can be called before its declaration appears in the file. That makes refactoring easier, since I don’t have to worry as much about rearranging code when functions depend on one another. Because I already use this style in utility files and within component and hook implementations, it also feels more consistent to use function declarations for custom hooks and components.

Most applications can get surprisingly far with components, hooks, and context alone. But eventually state management becomes complex enough that you need to reach for another pattern: reducers.

Reducers: When State Gets Complicated

Sometimes useState isn’t enough. When you’ve got a pile of state updates scattered across a dozen event handlers, useReducer lets you consolidate all that logic into a single function and reason about it in one place.

The keyword there is complicated. A reducer that just flips a boolean isn’t a reducer, it’s component state wearing a costume. Reach for a reducer when the state logic is genuinely complicated. For example, form state, state the need to change together because they affect each other, loading/error states, when you need to fetch configuration from an API then the client can change it, and many other complexities.

My rule of thumb, on whether it’s complex, is:

  1. Multiple states need to change together.
  2. There are more than 2 or 3 actions required to update it. Meaning you are calling for state updates in more than 2 or 3 places, and they’re each slightly different payloads.

When it does, I give each reducer its own kebab-case folder under reducers/, named after the state it manages, with three files inside:

  • types.d.ts — the action types. This is where that global-declarations trick pays off: define your action union here, and you can use those types across the reducer and actions without importing them anywhere. An action always needs a type, and I use a payload for the inputs that determine the next state.
  • actions.ts — action creators, one function per action. Extracting these means that when the payload shape changes, I edit it in one place instead of hunting down every dispatch call in the app. Bonus: it’s strongly typed, so a bad payload lights up red immediately.
  • reducers.ts — the reducer function itself.

That last file has one non-negotiable rule. React compares the previous state value and next state value using Object.is. For objects and arrays, that effectively means React is comparing references rather than their contents. If you mutate the existing state object, React sees the same reference and shrugs; nothing re-renders. So a reducer must always return a new object:

case PlotlyDataActionTypes.UPDATE: {
const next = { ...state }; // shallow copy → new reference → React notices
// ...mutate the copy
return next;
}

The happy side effect of this whole structure is that reducers and actions are just pure functions. Pure functions are trivial to unit test—no rendering, no mocking the DOM, just given this state and this action, do I get the right state back?

A Few Conventions Worth Adopting Early

  • One component per file; the file is named after the component.
  • Barrel index.ts files — when they help (clean imports) and when they bite (circular deps, bundle bloat). Be honest about the tradeoff.
  • Keep components dumb, push logic into hooks/util. (This is the through-line into the next two posts — tease them.)

UI Goes Down, Events Come Up

Tying it all together is a single mental model for how data moves. Props flow down. Callbacks come up. UI components encapsulate their own markup and styles, staying focused on turning props into pixels. When something happens—a click, a change—they don’t reach out and mutate the outside world; they call a callback prop and let the parent decide what to do. The core data lives in external sources (APIs, context, reducers), and presentation components simply consume it.

Keep that boundary clean, and your app becomes easy to trace: data flows one direction, events flow the other, and you’re never left wondering who changed what.

Conclusion

None of these conventions are required. React is intentionally flexible, and plenty of successful teams organize their code differently. What matters is consistency. If developers can predict where code lives, understand how data flows, and keep rendering concerns separate from application logic, the project remains approachable long after the excitement of starting it has faded.

These same ideas show up again when designing components and building reusable UI systems, which is where I’ll head next.

This also comes in handy when designing components, but that’s a post for another day.