Skip to content

Commit cc96a1d

Browse files
committed
Merge branch 'redux' into 'master'
Add redux and redux-saga with documentation and an example Closes #4 and #5 See merge request tcm-projects/react-native-boilerplate!6
2 parents 89e61ba + 3ebefca commit cc96a1d

19 files changed

+1033
-28
lines changed

App/App.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
import React, { Component } from 'react'
2-
import { HomeScreen } from 'App/Containers/HomeScreen'
2+
import HomeScreen from 'App/Containers/HomeScreen'
3+
import { Provider } from 'react-redux'
4+
import { PersistGate } from 'redux-persist/lib/integration/react'
5+
import createStore from 'App/Stores'
6+
7+
const { store, persistor } = createStore()
38

49
export default class App extends Component {
510
render() {
6-
return <HomeScreen />
11+
return (
12+
/**
13+
* @see https://github.com/reduxjs/react-redux/blob/master/docs/api.md#provider-store
14+
*/
15+
<Provider store={store}>
16+
{/**
17+
* PersistGate delays the rendering of the app's UI until the persisted state has been retrieved
18+
* and saved to redux.
19+
* The `loading` prop can be `null` or any react instance to show during loading (e.g. a splash screen),
20+
* for example `loading={<SplashScreen />}`.
21+
* @see https://github.com/rt2zz/redux-persist/blob/master/docs/PersistGate.md
22+
*/}
23+
<PersistGate loading={null} persistor={persistor}>
24+
<HomeScreen />
25+
</PersistGate>
26+
</Provider>
27+
)
728
}
829
}

App/Containers/HomeScreen.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
import React from 'react'
2-
import { Platform, StyleSheet, Text, View } from 'react-native'
2+
import { Platform, StyleSheet, Text, View, Button } from 'react-native'
3+
import { connect } from 'react-redux'
4+
import { PropTypes } from 'prop-types'
5+
import ExampleActions from 'App/Stores/Example/Actions'
6+
import { isHot } from 'App/Stores/Example/Selectors'
37

48
const instructions = Platform.select({
59
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
610
android: 'Double tap R on your keyboard to reload,\n' + 'Shake or press menu button for dev menu',
711
})
812

9-
export class HomeScreen extends React.Component {
13+
class HomeScreen extends React.Component {
14+
componentDidMount() {
15+
this.props.fetchTemperature()
16+
}
17+
1018
render() {
19+
let temperature = this.props.temperatureIsLoading ? '...' : this.props.temperature
20+
if (temperature === null) {
21+
temperature = '??'
22+
}
23+
1124
return (
1225
<View style={styles.container}>
1326
<Text style={styles.welcome}>Welcome to React Native!</Text>
1427
<Text style={styles.instructions}>To get started, edit App.js</Text>
1528
<Text style={styles.instructions}>{instructions}</Text>
29+
<Text style={styles.instructions}>The weather temperature is: {temperature}</Text>
30+
<Text style={styles.instructions}>{this.props.isHot ? "It's pretty hot!" : ''}</Text>
31+
<Text style={styles.instructions}>{this.props.temperatureErrorMessage}</Text>
32+
<Button onPress={this.props.fetchTemperature} title="Refresh" />
1633
</View>
1734
)
1835
}
@@ -34,3 +51,24 @@ const styles = StyleSheet.create({
3451
marginBottom: 5,
3552
},
3653
})
54+
55+
HomeScreen.propsTypes = {
56+
temperature: PropTypes.number,
57+
temperatureErrorMessage: PropTypes.string,
58+
}
59+
60+
const mapStateToProps = (state) => ({
61+
temperature: state.example.get('temperature'),
62+
temperatureErrorMessage: state.example.get('temperatureErrorMessage'),
63+
temperatureIsLoading: state.example.get('temperatureIsLoading'),
64+
isHot: isHot(state),
65+
})
66+
67+
const mapDispatchToProps = (dispatch) => ({
68+
fetchTemperature: () => dispatch(ExampleActions.fetchTemperature()),
69+
})
70+
71+
export default connect(
72+
mapStateToProps,
73+
mapDispatchToProps
74+
)(HomeScreen)

App/Sagas/ExampleSaga.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { put, call } from 'redux-saga/effects'
2+
import ExampleActions from 'App/Stores/Example/Actions'
3+
import { WeatherService } from 'App/Service/WeatherService'
4+
5+
/**
6+
* A saga can contain multiple functions.
7+
*
8+
* This example saga contains only one to fetch the weather temperature.
9+
*/
10+
export function* fetchTemperature() {
11+
// Dispatch a redux action using `put()`
12+
// @see https://redux-saga.js.org/docs/basics/DispatchingActions.html
13+
yield put(ExampleActions.fetchTemperatureLoading())
14+
15+
// Fetch the temperature from an API
16+
const temperature = yield call(WeatherService.fetchTemperature)
17+
18+
if (temperature) {
19+
yield put(ExampleActions.fetchTemperatureSuccess(temperature))
20+
} else {
21+
yield put(
22+
ExampleActions.fetchTemperatureFailure('There was an error while fetching the temperature.')
23+
)
24+
}
25+
}

App/Sagas/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[Redux Saga](https://redux-saga.js.org/) is a library that makes application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage.
2+
3+
This directory contains the sagas of the application. Sagas will for example connect to an API to fetch data, perform actions, etc.

App/Sagas/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { takeLatest } from 'redux-saga/effects'
2+
import { ExampleTypes } from 'App/Stores/Example/Actions'
3+
import { fetchTemperature } from './ExampleSaga'
4+
5+
export default function* root() {
6+
yield [
7+
/**
8+
* @see https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
9+
*/
10+
// Call `fetchTemperature()` when a `FETCH_TEMPERATURE` action is triggered
11+
takeLatest(ExampleTypes.FETCH_TEMPERATURE, fetchTemperature),
12+
]
13+
}

App/Service/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This directory contains services that are meant to connect the application to other APIs, for example

App/Service/WeatherService.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { create } from 'apisauce'
2+
3+
const weatherApiClient = create({
4+
baseURL: 'https://query.yahooapis.com/v1/public/',
5+
headers: {
6+
Accept: 'application/json',
7+
'Content-Type': 'application/json',
8+
},
9+
timeout: 3000,
10+
})
11+
12+
function fetchTemperature() {
13+
// Simulate an error 50% of the time just for testing purposes
14+
if (Math.random() > 0.5) {
15+
return new Promise(function(resolve, reject) {
16+
resolve(null)
17+
})
18+
}
19+
20+
const locationQuery = escape(
21+
"select item.condition.temp from weather.forecast where woeid in (select woeid from geo.places where text='Lyon, Rhone-Alpes, FR') and u='c'"
22+
)
23+
24+
return weatherApiClient.get('yql?q=' + locationQuery + '&format=json').then((response) => {
25+
if (response.ok) {
26+
return response.data.query.results.channel.item.condition.temp
27+
}
28+
29+
return null
30+
})
31+
}
32+
33+
export const WeatherService = {
34+
fetchTemperature,
35+
}

App/Stores/CreateStore.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { applyMiddleware, compose, createStore } from 'redux'
2+
import createSagaMiddleware from 'redux-saga'
3+
import { persistReducer, persistStore } from 'redux-persist'
4+
import immutableTransform from 'redux-persist-transform-immutable'
5+
6+
/**
7+
* This import defaults to localStorage for web and AsyncStorage for react-native.
8+
*
9+
* Kind in mind this storage *is not secure*. Do not use it to store sensitive information
10+
* (like API tokens, private and sensitive data, etc.).
11+
*
12+
* If you need to store sensitive information, use redux-persist-sensitive-storage.
13+
* @see https://github.com/CodingZeal/redux-persist-sensitive-storage
14+
*/
15+
import storage from 'redux-persist/lib/storage'
16+
17+
const persistConfig = {
18+
transforms: [
19+
/**
20+
* This is necessary to support immutable reducers.
21+
* @see https://github.com/rt2zz/redux-persist-transform-immutable
22+
*/
23+
immutableTransform(),
24+
],
25+
key: 'root',
26+
storage: storage,
27+
/**
28+
* Blacklist state that we do not need/want to persist
29+
*/
30+
blacklist: [
31+
// 'auth',
32+
],
33+
}
34+
35+
export default (rootReducer, rootSaga) => {
36+
const middleware = []
37+
const enhancers = []
38+
39+
// Connect the sagas to the redux store
40+
const sagaMiddleware = createSagaMiddleware()
41+
middleware.push(sagaMiddleware)
42+
43+
enhancers.push(applyMiddleware(...middleware))
44+
45+
// Redux persist
46+
const persistedReducer = persistReducer(persistConfig, rootReducer)
47+
48+
const store = createStore(persistedReducer, compose(...enhancers))
49+
const persistor = persistStore(store)
50+
51+
// Kick off the root saga
52+
sagaMiddleware.run(rootSaga)
53+
54+
return { store, persistor }
55+
}

App/Stores/Example/Actions.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createActions } from 'reduxsauce'
2+
3+
/**
4+
* We use reduxsauce's `createActions()` helper to easily create redux actions.
5+
*
6+
* Keys are action names and values are the list of parameters for the given action.
7+
*
8+
* Action names are turned to SNAKE_CASE into the `Types` variable. This can be used
9+
* to listen to actions:
10+
*
11+
* - to trigger reducers to update the state, for example in App/Stores/Example/Reducers.js
12+
* - to trigger sagas, for example in App/Sagas/index.js
13+
*
14+
* Actions can be dispatched:
15+
*
16+
* - in React components using `dispatch(...)`, for example in App/App.js
17+
* - in sagas using `yield put(...)`, for example in App/Sagas/ExampleSaga.js
18+
*
19+
* @see https://github.com/infinitered/reduxsauce#createactions
20+
*/
21+
const { Types, Creators } = createActions({
22+
// Fetch the current weather temperature
23+
fetchTemperature: null,
24+
// The operation has started and is loading
25+
fetchTemperatureLoading: null,
26+
// The temperature was successfully fetched
27+
fetchTemperatureSuccess: ['temperature'],
28+
// An error occurred
29+
fetchTemperatureFailure: ['errorMessage'],
30+
})
31+
32+
export const ExampleTypes = Types
33+
export default Creators

App/Stores/Example/InitialState.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Map } from 'immutable'
2+
3+
/**
4+
* The initial values for the redux state.
5+
*/
6+
export const INITIAL_STATE = Map({
7+
temperature: null,
8+
temperatureErrorMessage: null,
9+
temperatureIsLoading: false,
10+
})

0 commit comments

Comments
 (0)