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.
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-blogThe 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 .
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
