Modular design for mobile and web development
Let's talk about
- React-Native is dope
- Sharing is caring
- (Re-)introducing modules
- Now code it
IOS + Android = React-Native
- Development in JavaScript
- Component approach based on React
- Same codebase for Android and IOS development
- One language to rule them all, one language to find them, one language to bring them all and in the darkness bind them.
React-Native is JS
- You can use (almost) all your favorite libraries
- Redux is still your store
- Unit testing use your favorite libraries: mocha, chai, jest, ...
- You can share code between your web and mobile
What we can share BETWEEN WEB AND MOBILE
- Most of community libraries
- Most of home-made libraries
- Resource client: React-Native provides the Fetch API
- Reducers, actions, sagas, ...
What we can't SHARE
- Libraries using DOM
- Router
- Views
But there are some issues :(
- How to architect our mobile and web applications ?
- How to handle web/mobile oriented libraries ?
- How to keep it maintainable ?
Your codebase
src
├── actions
│ ├── product.js
│ └── product.spec.js
├── containers
│ ├── Product.js
│ └── Product.spec.js
├── constants
│ ├── product.js
│ └── product.spec.js
├── components
│ ├── List.js
│ └── List.spec.js
├── reducers
│ ├── product.js
│ └── product.spec.js
└── app.js
Splitting your code in module
A module will contain all code relative to a FEATURE:
-
Feature's views (containers, components)
-
Feature's actions
-
Feature's reducer(s)
-
Feature's saga(s)
-
Feature's route(s)
- ...
Module's ADVANTAGES
- All your related stuff in same place
- More maintainable, especially when you
have a huge codebase
- Easily reusable
Structure
src
├── modules
│ └── product-list
│ ├── actions
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── components
│ │ ├── List.js
│ │ └── List.spec.js
│ ├── containers
│ │ ├── Product.js
│ │ └── Product.spec.js
│ ├── reducers
│ │ ├── index.js
│ │ └── index.spec.js
│ └── index.js
└── app.js
You don't share anything on day 1
You start by coding a web or a mobile app: avoid creating 9823 node_modules in the first month if you don't share anything at start.
Using module pattern in your src/ is a good thing.
It allows you to move to node_modules easily when the time comes.
How can you use your module ?
Your application:
- Needs to load some views
- Needs to load module's routes
- Needs to add module's reducers
- Needs to add module's saga(s)
Use import ?
We can import components and routes from module.
All modules have to know where data are stored.
Module can't assume their data will be in a specific
path of the store.
We need a place where we handle
path of module's reducers.
registry pattern
Your application's code
import registerProductList from './modules/product-list';
registerProductList();
Module's codeimport reducers from './reducers';
import sagas from './sagas';
const registerProductList = () => {
registerReducer('myModuleName', reducers);
registerSagas(sagas);
};
export default registerProductList;
dependencies
Advantages
- Your module can ignore where it will be used: your mobile app, your web app, your back-office app, ...
- Easily removable
- Good pattern to access to a module's reducer from another module
Reducers, Sagas:
redux-register-module
A simple module to register your module reducers/sagas
- Expose registerModuleReducer(key, reducer)
to register a reducer
- Expose getModuleState(key, state) to get module
state
- Expose a registerModuleSaga(saga)
- Lightweight
Plug
products/index.js
import { registerModuleReducer } from 'react-register-module';
import reducer from './reducer.js';
export default () => {
registerModuleReducer('products', reducer);
}
src/store/configureStore.js
import registerProducts from 'products/reducer';
import createRootReducer from '../reducers';
const configureStore = (initialState) => {
registerProducts(); // Register the products reducer
const rootReducer = createRootReducer();
};
Plug (2)
src/reducers.js
import { combineReducers } from 'redux';
import { getModuleReducerKey, getReducers } from 'redux-register-module';
const createRootReducer = combineReducers({
[getModuleReducerKey()]: combineReducers(getReducers()),
});
export default createRootReducer;
Play
users/components/web/UserList.js
import { getModuleState } from 'react-register-module';
import { connect } from 'redux';
const mapStateToProps = (state) => ({
products: getModuleState('products', state).list,
users: getModuleState('users', state).list,
});
...
And of course, you can use reselect if you want
import { getModuleState } from 'react-register-module';
const productsSelector = state => getModuleState('products', state).list;
...
Routes / Entrypoint
- Your mobile app have to instantiate first containers
- Your web app needs module's routes
Handle views
- A module contains mobile and web views
- A good way to start is to export views in different files
- web.js for React components
- mobile.js for React Native components
Module structure
products
├── ...
├── components
│ ├── mobile
│ │ ├── ProductIconList.js
│ │ └── ProductIconList.spec.js
│ └── web
│ ├── ProductList.js
│ ├── ProductList.spec.js
│ ├── Product.js
│ └── Product.spec.js
├── ...
├── actions.js
├── constants.js
├── mobile.js
└── web.js
A graph (Views)
Inter-module imports
import { ProductList } from 'products/web'; // Web components
import { ProductIconList } from 'products/mobile'; // Mobile components
import { getProducts } from 'products/actions'; // Actions
import { PRODUCT_SIZE } from 'products/constants'; // Constants
Conclusion
- Start with "src" modules, move to npm when you scale
- Your app imports the module and registers it
- Your module ignores where data are and use registry
- Separate web and mobile components but they are in the same module, using same actions and same reducers
- Don't make micro-modules, it will be pain in the ass to maintain
Thank you for your attention
Clément Danjou
Software Consultant
https://clement.danjou.io/
Twitter @cledanjou