How to Build a React Project from Scratch: Architecture, Tools, and Best Practices

This article walks through creating a React application without scaffolding tools, detailing the project directory layout, chosen tech stack—including React, Redux, Immutable.js, redux‑saga, and axios—along with development utilities like redux‑devtools and Reactotron, component organization, routing strategies, state persistence, and asynchronous task management.

21CTO
21CTO
21CTO
How to Build a React Project from Scratch: Architecture, Tools, and Best Practices

Introduction

Many developers rely on scaffolding tools such as create‑react‑app, which quickly generate a React project structure but hide the underlying architecture and may not meet specific business requirements. Building a project from zero gives deeper control over the architecture and technology stack.

Project Structure and Tech Stack

We start by cloning the repository and creating the directory layout manually:

git clone https://github.com/codingplayboy/react-blog.git
cd react-blog

The resulting structure includes:

src : source code

webpack : webpack configuration

webpack.config.js : entry for webpack

package.json : dependency management

yarn.lock : lock file

.babelrc : Babel configuration

eslintrc / eslintignore : ESLint settings

postcss.config.js : PostCSS configuration

API.md : API documentation entry

docs : documentation

README.md : project description

The chosen tech stack consists of:

React and react‑dom

react‑router for routing

Redux as the state container, with react‑redux for binding

Immutable.js and redux‑immutable for immutable state

redux‑persist and redux‑persist‑immutable for persistence

redux‑saga for asynchronous task management

Jest, lodash, ramda for testing and utilities

Optional Reactotron for debugging

Development Debugging Tools

redux‑devtools

Install the extension and add it to the store configuration: yarn add --dev redux-devtools In development mode, replace the default compose with the extension’s enhancer:

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

Reactotron

Install and configure Reactotron:

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

Four component types are recommended:

Layout components : only structure UI, no business logic.

Container components : fetch data and handle logic, render presentation components.

Presentation components : display UI.

UI components : reusable, usually stateless.

Redux

Redux provides a predictable state container. Key concepts include:

Single source of truth (the store).

State tree organized like a DOM tree.

Actions as plain objects with type and payload.

Reducers as pure functions handling actions. dispatch to send actions. createStore to instantiate the store.

Middleware and Enhancers

Middleware intercepts actions before reducers. Example enhancer that logs actions:

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;
};

Enhancers can also be used to extend the store itself.

react‑redux

Use Provider to inject the store and connect to bind components:

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

createStore with Middleware

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(saga);
  return store;
};

Redux and Immutable

When using Immutable.js, replace combineReducers with the version from redux‑immutable and initialize the store with an Immutable.Map:

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);
};

Reducers then work with immutable data, e.g.:

const initialState = Immutable.fromJS({ ids: [], posts: { list: [], total: 0, totalPages: 0 } });
export default (state = initialState, action) => {
  switch (action.type) {
    case 'RECEIVE_POST_LIST':
      return state.merge(action.payload);
    default:
      return state;
  }
};

React Router

React Router manages page navigation. Version 4 allows declarative routing inside components, enabling dynamic loading:

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>
);

To sync routing with Redux, use react‑router‑redux (or @next for v4) and a history object:

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)
);

Wrap routes with ConnectedRouter:

class Routes extends Component {
  render() {
    return (
      <ConnectedRouter history={history}>
        <div>
          <BlogHeader />
          <Route exact path='/' component={Home} />
          <Route exact path='/posts/:id' component={Article} />
        </div>
      </ConnectedRouter>
    );
  }
}

Navigation can be triggered via dispatch(push('/about')).

Redux Persistence

Use redux‑persist (or redux‑persist‑immutable for immutable state) to store the Redux state in localStorage and rehydrate on app start:

import { persistStore } from 'redux-persist-immutable';
if (ReduxPersistConfig.active) {
  RehydrationServices.updateReducers(store);
}

Configuration example:

persistStore(store, {
  transforms: [immutablePersistenceTransform]
}, startApp);

The transform converts between Immutable and plain JS:

import R from 'ramda';
import Immutable, { Iterable } from 'immutable';
const convertToJs = (state) => state.toJS();
const fromImmutable = R.when(Iterable.isIterable, convertToJs);
const toImmutable = (raw) => Immutable.fromJS(raw);
export default {
  out: (state) => toImmutable(state),
  in: (raw) => fromImmutable(raw)
};

Asynchronous Task Management

We use axios for HTTP requests and redux‑saga to orchestrate side effects.

axios

Axios provides a Promise‑based HTTP client that works in browsers and Node, supports request/response interception, cancellation, and automatic JSON conversion.

redux‑saga

Initialize saga middleware and run the root saga:

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

Root saga forks individual module sagas:

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

Example of an app saga handling post list requests:

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);
}

HTTP helper used by the saga:

function getPostList(payload) {
  return fetch({ ...API.getPostList, data: payload }).then(res => {
    if (res) {
      const data = formatPostListData(res.data);
      return {
        total: parseInt(res.headers['x-wp-total'], 10),
        totalPages: parseInt(res.headers['x-wp-totalpages'], 10),
        ...data
      };
    }
  });
}

Reactotron with saga

Attach a saga monitor when Reactotron is enabled:

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

Conclusion

The article provides a comprehensive walkthrough of building a React application from the ground up, covering project scaffolding, state management with Redux and Immutable, routing, persistence, debugging tools, and asynchronous flow control with redux‑saga, offering a solid foundation for front‑end engineers.

Note: The source code repository is https://github.com/codingplayboy/react-blog .
ReduxReActWebpackFrontend ArchitectureDebugging Toolsredux-sagaImmutable.jsstate persistence
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.