Topic 2: React Structure
📖 6 min read · 🎯 beginner · 🧭 Prerequisites: mac-installation, node-projects
Why this matters
Up until now, you've probably been dropping all your React files into one folder and thinking, "I'll sort this out later." I did the same thing when I started. But the moment your app grows past a handful of components, that single folder becomes a nightmare — you spend more time hunting for files than actually building features. A well-organized React project structure is not a nice-to-have; it's what separates a project you can maintain from one you're afraid to touch. Today we're going to walk through how a real React app is laid out — components, hooks, context, services, and utilities — so you always know exactly where everything lives.
What You'll Learn
- Understand the default file layout generated by Create React App
- Organize components by feature using a
src/components/hierarchy - Structure static assets under
src/assets/for images, fonts, and global styles - Create and locate custom hooks in
src/hooks/and context providers insrc/context/ - Separate API calls and utility functions into
src/services/andsrc/utils/ - Wire all districts together in a clean top-level
App.js
The Analogy
Think of a React project as a city borough map. Every borough — Components, Assets, Hooks, Context, Services, Utils — has a specific purpose, and residents (files) belong in the borough that matches their role. A Button component lives in the Components district just as a post office lives in the Civic district, not scattered randomly across the map. When a new developer joins the team, they can navigate the codebase the same way a visitor navigates a well-labeled city: follow the signs, find the right district, locate the exact building. The larger the city grows, the more valuable the zoning becomes — without it, every search turns into an expedition.
Chapter 1: The Default Create React App Layout
the trainer pulled up the terminal. "Every mission starts at base camp," she said. When you scaffold a new project with Create React App, you get this starting layout:
my-react-app/
├── node_modules/
├── public/
│ ├── index.html
│ └── ...
├── src/
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ └── ...
├── .gitignore
├── package.json
├── README.md
└── yarn.lock
Key districts to know:
public/— static files served as-is;index.htmlis the single HTML shell.src/— everything you write lives here; Webpack bundles it.node_modules/— third-party packages; never edit these directly.package.json— project manifest: dependencies, scripts, metadata.
This default layout works for toy apps, but as features multiply, dumping every file directly under src/ becomes unmanageable. That is where intentional structure takes over.
Chapter 2: Organizing Components by Feature
"Districts are only useful if residents actually stay in them," alex said. The recommended convention is to group components by the feature they belong to, with shared reusable pieces collected under common/.
src/
├── components/
│ ├── common/
│ │ ├── Button.js
│ │ ├── Input.js
│ │ └── ...
│ ├── Home/
│ │ ├── Home.js
│ │ ├── Home.css
│ │ └── ...
│ ├── About/
│ │ ├── About.js
│ │ ├── About.css
│ │ └── ...
│ └── ...
Each feature folder owns its component file and its stylesheet. This colocation means you never have to hunt across the project to find the CSS that styles a given component.
Here is the Home component in its simplest form:
src/components/Home/Home.js
import React from 'react';
import './Home.css';
function Home() {
return (
<div className="home">
<h1>Welcome to Vizag</h1>
</div>
);
}
export default Home;
src/components/Home/Home.css
.home {
text-align: center;
padding: 20px;
}
The component imports its own stylesheet directly — no global side-effects, no mystery about where that .home class comes from.
Chapter 3: Organizing Assets
Static assets — images, fonts, global CSS variables — belong in src/assets/, partitioned by type:
src/
├── assets/
│ ├── images/
│ │ ├── logo.png
│ │ └── ...
│ ├── fonts/
│ │ ├── custom-font.ttf
│ │ └── ...
│ └── styles/
│ ├── variables.css
│ ├── mixins.css
│ └── ...
Webpack (bundled with Create React App) treats imported image files as module references, resolving them to their hashed output path at build time. Import an image the same way you import a module:
src/components/Home/Home.js (updated with logo)
import React from 'react';
import './Home.css';
import logo from '../../assets/images/logo.png';
function Home() {
return (
<div className="home">
<img src={logo} alt="Logo" />
<h1>Welcome to Vizag</h1>
</div>
);
}
export default Home;
The ../../assets/images/logo.png path walks up two levels from src/components/Home/ to reach src/assets/images/. If you ever move the component, the broken import immediately signals that the path needs updating — intentional, helpful friction.
Chapter 4: Custom Hooks and Context Providers
"Reusable logic that isn't a component lives in hooks/," alex explained. "Shared state that spans the component tree lives in context/."
Custom Hooks — src/hooks/
src/
├── hooks/
│ ├── useFetch.js
│ └── useLocalStorage.js
src/hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
useFetch encapsulates the entire fetch lifecycle — loading state, error state, and result — so any component can call const { data, loading, error } = useFetch(url) without repeating that boilerplate.
Context Providers — src/context/
src/
├── context/
│ ├── ThemeContext.js
│ └── AuthContext.js
src/context/ThemeContext.js
import React, { createContext, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export default ThemeContext;
ThemeContext exposes both the current theme string and the setTheme setter. Any deeply-nested component can consume this context without prop-drilling through every intermediate layer.
Chapter 5: Services and Utilities
"API calls and pure helper functions deserve their own districts," alex said, highlighting two final folders.
API Services — src/services/
src/
├── services/
│ ├── api.js
│ └── auth.js
src/services/api.js
export const fetchData = async (endpoint) => {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return data;
};
Centralizing fetch logic here means one place to add auth headers, base URLs, or error interceptors — instead of sprinkling fetch() calls across dozens of components.
Utility Functions — src/utils/
src/
├── utils/
│ ├── helpers.js
│ └── validators.js
src/utils/helpers.js
export const capitalize = (str) => {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
};
Pure functions with no React dependency live here. They are easy to unit-test in isolation and reusable from any component, hook, or service.
Chapter 6: Putting It Together — App.js
The final integration point is src/App.js. It imports from every district and composes them into the application tree:
src/App.js
import React from 'react';
import './App.css';
import Home from './components/Home/Home';
import { ThemeProvider } from './context/ThemeContext';
function App() {
return (
<ThemeProvider>
<div className="App">
<Home />
</div>
</ThemeProvider>
);
}
export default App;
ThemeProvider wraps the entire tree at the top level so that any child component can access the theme without any additional configuration. Home renders inside that provider and can consume ThemeContext through useContext whenever it needs to.
Here is the full directory map of the mature structure:
src/
├── assets/
│ ├── images/
│ ├── fonts/
│ └── styles/
├── components/
│ ├── common/
│ ├── Home/
│ └── About/
├── context/
│ ├── ThemeContext.js
│ └── AuthContext.js
├── hooks/
│ ├── useFetch.js
│ └── useLocalStorage.js
├── services/
│ ├── api.js
│ └── auth.js
├── utils/
│ ├── helpers.js
│ └── validators.js
├── App.css
├── App.js
└── index.js
graph TD
A[src/index.js] --> B[src/App.js]
B --> C[context/ThemeContext.js]
B --> D[components/Home/Home.js]
D --> E[assets/images/logo.png]
D --> F[hooks/useFetch.js]
F --> G[services/api.js]
D --> H[utils/helpers.js]
🧪 Try It Yourself
Task: Scaffold the full district structure inside an existing Create React App project and verify each folder is wired up correctly.
- Inside your
src/folder, create the following directories:mkdir -p src/assets/images src/assets/styles mkdir -p src/components/common src/components/Home mkdir -p src/hooks src/context src/services src/utils - Copy the
useFetch.jsexample above intosrc/hooks/useFetch.js. - Add a
console.logcall insideuseFetchjust before thereturnstatement:console.log('useFetch result:', { data, loading, error }); return { data, loading, error }; - In
src/components/Home/Home.js, call the hook with a public API:import React from 'react'; import useFetch from '../../hooks/useFetch'; function Home() { const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1'); if (loading) return <p>Loading...</p>; if (error) return <p>Error!</p>; return <h1>{data?.title}</h1>; } export default Home; - Run
npm start.
Success criterion: You should see useFetch result: { data: {...}, loading: false, error: null } in the browser console, and the todo title rendered on screen — confirming the hook, component, and directory structure are all correctly wired.
🔍 Checkpoint Quiz
Q1. Why is it better to colocate a component's CSS file inside its own feature folder (e.g., Home/Home.css) rather than placing all CSS in a single global stylesheet?
Q2. Given this import at the top of src/components/Home/Home.js:
import logo from '../../assets/images/logo.png';
What does the ../../ prefix navigate past, and what would break if you changed it to ../assets/images/logo.png?
A) It navigates past components/Home/ to reach src/; removing one ../ would look for assets/ inside components/ instead of src/, causing a module-not-found error.
B) It navigates past src/ to the project root; removing one ../ makes no difference.
C) Webpack resolves all image paths automatically so the prefix does not matter.
D) It navigates past node_modules/; changing it would point to a different package.
Q3. What would you change about the fetchData function in src/services/api.js if every request to your API needed an Authorization: Bearer <token> header?
Q4. Look at this useFetch snippet:
useEffect(() => {
fetchData();
}, [url]);
What happens if you change [url] to []? Choose the best answer.
A) The fetch runs on every render, causing an infinite loop.
B) The fetch runs only once when the component mounts; changing the url prop later will not trigger a new fetch.
C) The fetch never runs because the dependency array is empty.
D) React throws an error because useEffect requires at least one dependency.
A1. Colocation keeps the component's styles alongside its logic, so deleting or moving the component takes its CSS with it automatically. A single global stylesheet grows into a tangled namespace where rules affect components unexpectedly.
A2. A) — ../../ goes up from Home/ to components/, then up again to src/, which is where assets/ lives. One ../ only escapes Home/, landing inside components/, where there is no assets/ folder.
A3. Add a headers option to the fetch call, reading the token from wherever it is stored (e.g., localStorage or a passed parameter):
export const fetchData = async (endpoint, token) => {
const response = await fetch(endpoint, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
Centralizing this in services/api.js means you update the header logic in one place.
A4. B) — An empty dependency array [] means the effect runs once after the initial render and never again. If the parent passes a new url value, useFetch will ignore it and keep returning the original data.
🪞 Recap
- Create React App's default layout is a starting point — as features grow, partition
src/intocomponents/,assets/,hooks/,context/,services/, andutils/. - Group components by feature and colocate their CSS; shared primitives go in
components/common/. - Custom hooks encapsulate stateful logic (like fetching) so multiple components can reuse it without duplicating code.
- Context providers in
src/context/share application-wide state (theme, auth) without prop-drilling. - API calls belong in
src/services/and pure helper functions insrc/utils/— both are easy to test and update in a single location.
📚 Further Reading
- Create React App docs — the source of truth on the default folder structure
- React docs — Custom Hooks — official guide to extracting and reusing hook logic
- React docs — Context — deep dive into
createContextanduseContext - Bulletproof React — a community-maintained opinionated project structure reference (PLACEHOLDER if URL changes)
- ⬅️ Previous: Node Projects
- ➡️ Next: PHP with Select Fetch