Choosing the proper react / redux project structure
December 8, 2017
Not so hot way
Typical structure for example project looks something like this:
components/
TodoList.js
containers/
TodoContainer.js
reducers/
todoReducer.js
actions/
todoActions.js
constants/
todoContants.js
selectors/
todoSelectors.js
sagas/
todoSaga.js
index.js
Files are grouped by technology roles not by features.
It is completely fine to use it for small projects but as project grows this won’t scale up. Having more than 10 files in directory makes searching difficult, also import autocomplete becomes a hassle with dozen of options available.
Features oriented over everything else
Personally I recommend and use below structure:
src/
components/ -> common dumb components that can be reused
Header/
index.js
Menu/
index.js
containers/
Todo/
tests/
todoContainer.test.js
index.js -> container component
TodoList -> dumb component that is used only for todo
todoDuck.js -> action constants, action creators, reducer
selectors.js -> reselect code
saga.js -> redux-saga
AnotherFeature/
.
.
.
css/ -> css related files if not using css-in-js
assets/ -> images, fonts etc.
utils/ -> code that doesn't fit into any other place
index.js -> entry point
configureStore.js -> redux store configuration
reducers.js -> combine reducers
.env -> contains 'NODE_PATH=src/', needed for absolute imports
package.json
README.md
Everything is divided by features, it scales by adding new directories per feature and not having +10 files in directories.
It uses index.js files because typically a feature in redux is seen as a single container file, this way you can easily import something by doing import Todo from containers/Todo
.
Other files in feature directory are most of the times imported withing this directory.
I also use duck convention so redux constants, action creators and reducers are in one file under specific feature.
It eliminates context switching and I don’t like having 10 lines of imports in each redux file. More about it here. Of course selectors or sagas are optional and if I’m using redux-thunk, it’s logic should be placed in duck files under action creators section.
// Actions
const LOGIN_REQUEST = "containers/login/LOGIN_REQUEST";
const LOGIN_REQUEST_SUCCESS = 'containers/login/LOGIN_REQUEST_SUCCESS';
const LOGIN_REQUEST_FAIL = 'containers/login/LOGIN_REQUEST_FAIL';
// Reducer
export default function reducer(state = {
isFetching: false,
isFailing: false,
isLoginSuccessful: false,
jwtToken: ""
}, action = {}) {
switch (action.type) {
case LOGIN_REQUEST:
return { ...state, isFetching: true };
case LOGIN_REQUEST_SUCCESS:
return { ...state, isFetching: false, isFailing: false, isLoginSuccessful: true, jwtToken: action.token.token };
case LOGIN_REQUEST_FAIL:
return { ...state, isFailing: true };
default:
return state;
}
}
// Action Creators
function loginRequest() {
return { type: LOGIN_REQUEST };
}
function loginRequestSuccess(token, expiryDate) {
return { type: LOGIN_REQUEST_SUCCESS, token, expiryDate };
}
function loginRequestFail(error) {
return { type: LOGIN_REQUEST_FAIL, error };
}
export const fetchLogin = (login, pass) => async dispatch => {
try {
dispatch(loginRequest());
let response = await fetchPost(loginUrl, { login, password: pass });
if (!response.ok) {
dispatch(loginRequestFail(response.statusText));
return;
}
let json = await response.json();
dispatch(loginRequestSuccess(json, null));
} catch (ex) {
dispatch(loginRequestFail(ex.toString()));
}
};
Additionaly to make absolute imports
work I have .env file in root directory:
NODE_PATH=src/
This file is used by default in create-react-app so if your using it nothing webpack'y
has to be configured.
File naming convention
In the past I was using kebab-case for both files and directories no matter what was inside. I wanted to have same convention for server-side code (nodejs) and front-end code (react).
But I took a different direction and went with community, deciding to use a convention that will tell from just watching at the name of a file or directory what is inside.
- if a file is exporting something that can be instantiated (class, component) then use PascalCase
- index.js files are lower-case
- everything else is camelCase
- test files are pascalCase.test.js
- directories containing index.js, which fulfil point number 1, should be PascalCase
- every other directory should be camelCase
Try it out and Cheeeers!