Building a React Project Structure from Scratch: A Comprehensive Guide

This article walks through creating a React application without scaffolding tools, detailing project directory layout, chosen technology stack—including React, Redux, Immutable.js, react-router, redux-saga, and related debugging and persistence utilities—while explaining component design, middleware, enhancers, routing, and asynchronous task management.

Architecture Digest
Architecture Digest
Architecture Digest
Building a React Project Structure from Scratch: A Comprehensive Guide

Introduction

Many scaffolding tools such as create-react-app can generate a React project with a single command, but they hide the learning opportunity of understanding the full project architecture and technology stack. To gain deeper control, this guide builds a React application from 0 to 1.

Project Structure and Technology Stack

We start without any scaffolding, creating each file and importing each third‑party library manually. The resulting directory layout is:

src/               // source code
webpack/           // webpack configuration
webpack.config.js // webpack entry file
package.json      // dependency management
yarn.lock         // lock file for exact versions
.babelrc          // Babel configuration for JSX and ES6
.eslintrc & .eslintignore // ESLint configuration
postcss.config.js // PostCSS configuration
API.md            // API documentation entry
docs/              // documentation folder
README.md          // project description

The src folder will later be populated with modules, routing, state management, and tests.

Chosen Tech Stack

The stack is selected based on eight considerations:

React and React‑DOM

React Router for routing

Redux as the state container

Immutable.js (optional) for immutable state

Redux‑Persist for state persistence

Redux‑Saga for asynchronous task handling

Jest and utility libraries (lodash, ramda) for testing

Reactotron (optional) for debugging

Development and Debugging Tools

redux‑devtools

Install the browser extension and add the enhancer to the store creation code:

yarn add --dev redux-devtools
import { applyMiddleware, compose, createStore, combineReducers } from 'redux';
let composeEnhancers = compose;
if (__DEV__) {
  const devExt = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
  if (typeof devExt === 'function') {
    composeEnhancers = devExt;
  }
}
const store = createStore(combineReducers(...), initialState, composeEnhancers(applyMiddleware(...middleware), ...enhancers));

Reactotron

Reactotron provides a desktop client for real‑time monitoring of Redux actions, sagas, and network requests.

yarn add --dev reactotron-react-js
import Reactotron from 'reactotron-react-js';
import { reactotronRedux as reduxPlugin } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga';
if (Config.useReactotron) {
  Reactotron.configure({ name: 'React Blog' })
    .use(reduxPlugin({ onRestore: Immutable }))
    .use(sagaPlugin())
    .connect();
  Reactotron.clear();
  console.tron = Reactotron;
}

Wrap the root component to enable the overlay:

import './config/ReactotronConfig';
import DebugConfig from './config/DebugConfig';
class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <AppContainer />
      </Provider>
    );
  }
}
export default DebugConfig.useReactotron ? console.tron.overlay(App) : App;

Component Division

React component design follows four roles:

Presentation Component

Container Component

Goal

UI rendering (HTML & CSS)

Business logic (data fetching, state updates)

Redux awareness

None

Yes

Data source

props

Redux store subscription

State change

Call props callbacks

Dispatch Redux actions

Reusability

Highly independent

Coupled to business logic

Redux

Redux is a predictable state container for JavaScript applications. Key concepts include a single source of truth, a store that holds the state tree, actions as plain objects, reducers as pure functions, and dispatch to trigger state changes.

Middleware

Middleware intercepts actions before they reach reducers, enabling logging, monitoring, routing, etc.

const logEnhancer = (createStore) => (reducer, preloadedState, enhancer) => {
  const store = createStore(reducer, preloadedState, enhancer);
  const originalDispatch = store.dispatch;
  store.dispatch = (action) => {
    console.log(action);
    originalDispatch(action);
  };
  return store;
};

Store Enhancers

Enhancers can extend the store beyond dispatch, allowing full customization of the store API.

react‑redux

The Provider component injects the store via React context, and connect creates higher‑order components that map state and dispatch to props.

class App extends Component {
  render() {
    const { store } = this.props;
    return (
      <Provider store={store}>
        <div><Routes /></div>
      </Provider>
    );
  }
}

createStore

Creating a store often involves applying middleware, saga middleware, and enhancers:

export default (rootReducer, rootSaga, initialState) => {
  const blogRouteMiddleware = routerMiddleware(history);
  const sagaMiddleware = createSagaMiddleware();
  const middleware = [blogRouteMiddleware, sagaMiddleware];
  const enhancers = [];
  let composeEnhancers = compose;
  const store = createStore(combineReducers({ router: routerReducer, ...reducers }), initialState, composeEnhancers(applyMiddleware(...middleware), ...enhancers));
  sagaMiddleware.run(rootSaga);
  return store;
};

Redux with Immutable.js

When the initial state is an Immutable.Map, the default combineReducers fails. Use redux-immutable to combine reducers that accept immutable data.

import { combineReducers } from 'redux-immutable';
import Immutable from 'immutable';
const initialState = Immutable.Map();
export default () => {
  const rootReducer = combineReducers({ ...RouterReducer, ...AppReducer });
  return configureStore(rootReducer, rootSaga, initialState);
};

React Router

React Router manages page‑level UI via declarative routes. Version 4 allows routes to be defined inside components, enabling dynamic routing and code‑splitting.

Static vs. Dynamic Routing

Older versions required a static route tree defined before rendering. v4 lets routes be declared at render time, improving flexibility.

import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  el
);
const App = () => (
  <div>
    <nav><Link to="/about">Dashboard</Link></nav>
    <Home />
    <Route path="/about" component={About} />
    <Route path="/features" component={Features} />
  </div>
);

Router Hooks

Instead of using onEnter or onLeave, component lifecycle methods such as componentDidMount can control routing.

Router Integration with Redux

Use react-router-redux (install @next for v4) to synchronize router state with the Redux store.

yarn add react-router-redux@next
yarn add history
import createHistory from 'history/createBrowserHistory';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
export const history = createHistory();
const middleware = routerMiddleware(history);
const store = createStore(combineReducers({ ...reducers, router: routerReducer }), applyMiddleware(middleware));
// In root component
<ConnectedRouter history={history}>...</ConnectedRouter>

Dispatch‑Based Navigation

import { push } from 'react-router-redux';
store.dispatch(push('/about'));

Redux Persistence

Persisting the Redux store across page reloads improves startup speed. The redux-persist library provides persistStore and autoRehydrate utilities.

yarn add redux-persist
// In store configuration
if (ReduxPersistConfig.active) {
  RehydrationServices.updateReducers(store);
}
// persistStore usage
persistStore(store, null, startApp);

Version handling stores a reducer version in localStorage and clears the store when the version changes.

Persisting Immutable State

Use redux-persist-immutable to handle immutable data structures, providing a transform that converts between JS objects and Immutable objects.

import { persistStore } from 'redux-persist-immutable';
import immutablePersistenceTransform from '../services/ImmutablePersistenceTransform';
persistStore(store, { transforms: [immutablePersistenceTransform] }, startApp);

Immutable.js

When Immutable.js is used, ensure the entire Redux state tree is immutable, persistence works with immutable data, and React Router state is also immutable.

Immutable Router Reducer Example

import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = Immutable.fromJS({ location: null });
export default (state = initialState, action) => {
  if (action.type === LOCATION_CHANGE) {
    return state.set('location', action.payload);
  }
  return state;
};

seamless‑immutable

As a lighter alternative to Immutable.js, seamless-immutable offers a more native‑like API while still providing immutability guarantees.

Asynchronous Task Management

HTTP requests are handled with axios, a Promise‑based client supporting both browser and Node environments.

yarn add axios

redux‑saga

redux‑saga is a middleware that manages side effects using generator functions.

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/';
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
const store = createStore(rootReducer, initialState, compose(applyMiddleware(...middleware)));
 sagaMiddleware.run(rootSaga);

Root sagas fork individual module sagas:

import { fork } from 'redux-saga/effects';
import { HomeSaga } from './Home/flux';
import { AppSaga } from './App/flux';
export default function* root() {
  yield [AppSaga, HomeSaga].map(saga => fork(saga));
}

Example of an app saga handling a post‑list request:

const REQUEST_POST_LIST = 'REQUEST_POST_LIST';
const RECEIVE_POST_LIST = 'RECEIVE_POST_LIST';
function requestPostList(payload) { return { type: REQUEST_POST_LIST, payload }; }
function receivePostList(payload) { return { type: RECEIVE_POST_LIST, payload }; }
function* getPostListSaga({ payload }) {
  const data = yield call(getPostList);
  yield put(receivePostList(data));
}
export function* AppSaga() {
  yield takeLatest(REQUEST_POST_LIST, getPostListSaga);
}

The getPostList function uses axios (or a wrapper) to fetch data and format the response.

saga monitoring with Reactotron

const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });

Conclusion

This article provides a detailed walkthrough of building a React project from the ground up, covering project architecture, state management with Redux, routing, persistence, immutability, debugging, and asynchronous task handling, offering a solid foundation for front‑end engineers to deepen their understanding of modern web application engineering.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptfrontend developmentReduxState ManagementReactwebpack
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.