Having trouble finding examples for integrating keys frameworks such has redux with saga and navigation on React Native. It is easy to find example on a specific topic but I could never find everything that would work together. I did some adaptation to my style and there are probably other ways, if you like or dislike, feel free to leave comments or suggestions.
React native skeleton project for:
- React Native
- React-Redux
- Redux-Persist
- React Navigation
- Redux-Saga
- Storybook
- Jest
If you are not interested in storybooks, you can use version 0.1
- Inspired by Spencer Carli tutorial
git clone https://github.com/syl20lego/rn-skeleton.git
cd rn-skeleton/
npm install
npm start or npm run storybook
react-native run-ios
react-native run-android
I'll try to show all the commands you need to run to create your own project in case you don't want to clone this project.
Building Projects with Native Code
npm install -g create-react-native-app
npm install -g react-native-cli
npm install -g react-native-git-upgrade
Create your own project.
react-native init RNskeleton
git init
git add .
git commit -m "initial"
npm install
npm install --save prop-types
yarn add prop-types
npm install --save react-redux redux redux-logger
npm install --save redux-persist
npm install --save redux-saga
yarn add react-redux redux redux-logger
yarn add redux-persist
yarn add redux-saga
npm install --save react-navigation
npm install --save react-navigation-redux-helpers
yarn add react-navigation
yarn add react-navigation-redux-helpers
npm install --save react-native-elements
npm install --save react-native-vector-icons
react-native link
yarn add react-native-elements
yarn add react-native-vector-icons
react-native link
react-native link
npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
npm install --save-dev jest-cli
npm install --save-dev nock
npm install --save-dev enzyme
npm install --save-dev enzyme-adapter-react-16
npm install --save-dev react-dom
yarn add --dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
yarn add --dev jest-cli
yarn add --dev nock
yarn add --dev enzyme
yarn add --dev enzyme-adapter-react-16
yarn add --dev react-dom
npm -g i @storybook/cli
yarn global add @storybook/cli
getstorybook
npm install
npm install -g jest
jest --watch
yarn run storybook
or
npm run storybook
react-native run-ios
react-native run-android
Show RN options
adb shell input keyevent 82
Reload RN
adb shell input text "RR"
From the application creation there are few choice you will have to make, I'll try to explains my decisions but sometime I'll just use the defacto.
Right from the start you have to make one big decision, either you use Expo or go Native Code way. The key to architecture is sometime it is better to delay the decision until it is important. Commiting to Expo right now seems to be a big steps and I wanted to decide which components to include along the way. I also wanted to be able to use my own native component in the future, I could always eject with Expo but would be leave with a big dependency. Since this is a learning journey, I decided to go the native code way.
React Native doesn't allow you to have anything other then alphanum chararcters in your project name, I named my repo rn-skeleton so I opt for RNskeleton and changed the directory name when creating the project Init.
Finaly this is the first version of react native 0.50 (0.54 now) that doesn't generate the index.android.js and index.ios.js. We can start editing the App.js directly. I want my files to be under /src rather then /app since there is alreay an app.json and an App.js.
In our application the store and the persistor are coming from our store component, it is pretty much same as PersistGate example usage
Application is our own application component.
App.js
render() {
const {persistor, store} = Store;
const onBeforeLift = () => {
// take some action before the gate lifts
};
// persistor.purge();
return (
<Provider store={store}>
<PersistGate
loading={<ActivityIndicator/>}
onBeforeLift={onBeforeLift}
persistor={persistor}>
<Application/>
</PersistGate>
</Provider>
);
}
We setup the store
store/index.js
const config = {
key: 'root',
storage,
};
const enhancers =
[applyMiddleware(
loggerMiddleware,
sagaMiddleware
)];
const persistConfig = {enhancers};
const store = createStore(persistCombineReducers(config, reducer), initialState, compose(...enhancers));
const persistor = persistStore(store, persistConfig);
sagaMiddleware.run(saga);
return {persistor, store};
In our reducers, we export the root reducer, but we keep each implementation inside their own reducer.
reducers/index.js
import navigator from './navigation.reducer';
import users from './users.reducer';
const rootReducer = {
navigator,
users
};
export default rootReducer;
Similar in our sagas we iterate and fork each implementation. Thanks to Jamie Sunderland proposal
sagas/index.js
import {fork} from 'redux-saga/effects';
import users from './users.saga';
const sagas = [
...users
];
export default function* root() {
yield sagas.map(saga => fork(saga));
}
Each saga can export a list of saga generators
sagas/user.saga.js
function* fetchUsersSaga() {
yield takeEvery(FETCH_USERS, fetchUsersEffect)
}
export default [fetchUsersSaga];
We javascript destructing for all actions in a common action creator
actions/index.js
import * as Users from './users.action';
export const ActionCreators = {
...Users
};
Since we are using saga, our action creator can be simple objects.
actions/users.action.js
export const fetchUsers = (page, seed) => {
return {
type: FETCH_USERS,
data: {
page,
seed
}
};
};
Our application is returning our root stack navigator, this is similar to React Navigation with Redux Integration
src/index.js
render() {
const { dispatch, navigator } = this.props;
return (
<Navigator
navigation={
addNavigationHelpers({
dispatch,
state: navigator
})
}
/>
)
}
}
const mapStateToProps = state => ({
navigator: state.navigator,
});
export default Application = connect(mapStateToProps)(AppWithNavigation);
Navigation reducer
reducer/navigation.reducer.js
import { NavigationActions } from 'react-navigation';
import Navigator from '../routes';
const initialState = Navigator.router.getStateForAction(NavigationActions.init);
export default (state = initialState, action) => {
const nextState = Navigator.router.getStateForAction(action, state);
return nextState || state;
};
Our application is using Tab navigation, therefore our Root navigator setup the tabs. We need headerMode to ensure we don't show another navigation bar.
routes/index.js
export default Navigator = StackNavigator(
{
Tabs: {
screen: Tabs
}
}, {headerMode: 'none'}
);
We setup 2 tabs
routes/index.js
const Tabs = TabNavigator(
{
HomeTab: {
screen: HomeStack,
},
InfoTab: {
screen: InfoStack,
}
}, {
}
);
To keep things clean, each tabs in contained in a separated file.
routes/home.route.js
export default HomeRoute = {
Home: {
screen: HomeScreen,
navigationOptions: ({navigation}) => ({
...tabs.item,
title: 'Home',
header: (Platform.OS === 'android') ? null : navigation.header,
})
},
Details: {
screen: DetailsScreen,
navigationOptions: ({navigation}) => ({
...tabs.item,
title: 'Details'
})
}
};
We can use destructing to reassemble the navigation stack.
routes/index.js
export const HomeStack = StackNavigator({
...HomeRoute
});
I like the idea of centralized styles for common component as well as colors scheme and fonts. I was inspired by react-native-weather
I' still not sure what should be common, I find having flex instructions and margin a bit cumbersome and they are probably better with the component.
Storybook is a nice framework to visualize your component without having to run the full application. This is useful for deep component inside your application that requires many steps to get to it. Therefore, it useful while developing your component and also it has the advantage to promote decouple components and provide an easy way to test.
We don't need android and iOS index anymore since React Native 0.50.
rm storybook/index.android.js
rm storybook/index.ios.js
Replace the index.js (under storybook) by the content of storybook.js (no need of the indirection)
rm storybook/index.js
mv storybook/storybook.js storybook/index.js
Move the stories undes the test folder.
mv storybook/stories test/
You can access the storybook via the browser http://localhost:7007/
By default storybook and stories are created under the same folder storybook, after cleaning up the old ios and android index files, I moved the stories under /test. This makes it easier to have all non production code under the same folder.
I like to keep thing organized separately so you have one stories file per components.
My preference is to have all test under the same directory structure (aka /test), this allow you to have the test separated by their categories: unit, integration. Since integration tests might require more time/setup. I also consider storybooks as non production code and kept it under the /test folder.
You need to configure JEST, setup file, ignore files
package.json
"jest": {
"preset": "react-native",
"setupFiles": [
"./__tests__/setup"
],
"testPathIgnorePatterns": [
"/node_modules/",
"./__tests__/setup"
],
"transformIgnorePatterns": [
"node_modules/(?!react-native|react-navigation)/"
],
"coverageReporters": [
"html",
"text"
]
}
__tests__/setup.js
In the setup, I use the following configuration
global.XMLHttpRequest = require("isomorphic-fetch");
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({ adapter: new Adapter() });
Actions are really easy to test, you just need to ensure they provide the correct object.
/test/unit/actions/users.actions.spec.js
it('Should create an action to fetch users', () => {
const page = 3;
const seed = 'ABC';
const expectedAction = {
type: types.FETCH_USERS,
data: {
page,
seed
}
};
expect(ActionCreators.fetchUsers(page, seed)).toEqual(expectedAction)
});
You can test javascript function, I like to use Nock for API testing HTTP mocking and expectations library
/test/unit/api/users.api.spec.js
it('API works correctly to fetch users', (done) => {
let seed = 22;
let page = 1;
nock('https://randomuser.me', {})
.get(`/api/?seed=${seed}&page=${page}&results=20`)
.reply(200, `
{"results": [{
"name": {
"first": "nicholas"
}
}]}`);
fetchUsers({seed, page})
.then((result) => {
expect(result.error).toBeNull();
expect(result.list).toHaveLength(1);
expect(result.list[0].name.first).toBe('nicholas');
})
.then(done)
.catch(done)
});
Because the reducers are simply managing states, they are easy to test and you just need to ensure they are creating the proper state given any actions
/test/unit/reducers/users.reducers.spec.js
it('should return the initial state', () => {
console.log('REDUCERS HERE !!!!', reducers.users);
expect(reducers.users(undefined, {})).toEqual(
{
"error": null,
"list": [],
"loading": false,
"page": 1,
"refreshing": false,
"seed": 1
}
)
});
Todo: React Component unit testing
App.spec.js
is an integration test, I haven't put much work into it yet, I found testing UX high maintenance.
Should we have redux integration testing instead ? Testing actions/reducers/sagas would be an option instead of UX testing.
Expo demonstrating how to build my app Capo Keys from scratch to deployment
- Thanks to Barry Michael Doyle for this excellent tutorial about developing an application from begining to the end using Expo and Redus. Very good explanations and enjoyable to watch
Simple React Native application with Redux
- Thanks to Jon Lebensold that provided me with the basic skeleton for redux.
createReducer.js
is interesting to simplify the reducer switch case. I haven't use it yet since it makes the reducers pattern looking different.
- Thanks to Spencer Carli to giving the UX idea and api as well as the good explanation how to setup FlatList
What is the right way to do asynchronous operations in Redux?
- Provides good overview of different solutions for asynchronous framework working with redux. In the end, I select redux-sagas since I like keeping my actions being plain object and reducers simple and only handling states
React Native Elements Cross Platform React Native UI Toolkit
React Navigation with Redux Integration
Storybook is a development environment for UI components
HTTP mocking and expectations library
macOS High Sierra Version 10.13.3
npm --version 6.0.0
node --version v8.6.0
react-native --version react-native-cli: 2.0.1 react-native: 0.54.4