Hello everyone, today in this article we will take care of managing the application's settings. We will therefore start by adding Redux, and while configuring it we will create all the building blocks needed for settings management (reducers, actions, etc.). Finally, we will integrate all of this into our Settings screen.
Installing Redux
The first thing to do is install redux and the other related libraries:
yarn add redux react-redux redux-logger redux-persist redux-thunkNext, set up the folder tree for the Redux building blocks:
cd src
mkdir actions reducers storeActions
Now that Redux is installed, we will work on the actions. Because we use Flow, we start by creating a types.js file in the actions folder.
This file will contain the different types for our actions:
/**
* @flow
*/
export type Dispatch = (action: Action | ThunkAction | Array<Action>) => any
export type GetState = () => Object
export type ThunkAction = (dispatch: Dispatch, getState: GetState) => any
/*
**************
* Settings
**************
*/
export type SetPasswordLengthAction = {
type: 'SET_PASSWORD_LENGTH',
length: number,
}
export type SetAutoGenerationAction = {
type: 'SET_PASSWORD_LENGTH',
autoGeneration: boolean,
}
export type Action =
/** *** Settings **** */
SetPasswordLengthAction | SetAutoGenerationActionHere, first, we create two Flow types, SetPasswordLengthAction and SetAutoGenerationAction. Each represents an action, and for each one we find the type and the value returned by the action.
Then we define an Action type, which will be the type of the actions for each reducer. This Action type can be SetPasswordLengthAction or SetAutoGenerationAction; we define that with the | notation.
We will create the settings.js file in the actions folder. It will contain the actions and action creators for the Settings part:
/*
* @flow
*/
import type { ThunkAction, Dispatch, SetPasswordLengthAction, SetAutoGenerationAction } from './types'
/*
*** Actions ***
*/
export const SetPasswordLength = (length: number): ThunkAction => (dispatch: Dispatch) => {
dispatch(setPasswordLength(length))
}
export const SetAutoGeneration = (autoGeneration: boolean): ThunkAction => (dispatch: Dispatch) => {
dispatch(setAutoGeneration(autoGeneration))
}
/*
*** Actions Creator ***
*/
const setPasswordLength = (length: number): SetPasswordLengthAction => ({
type: 'SET_PASSWORD_LENGTH',
length,
})
const setAutoGeneration = (autoGeneration: boolean): SetAutoGenerationAction => ({
type: 'SET_AUTO_GENERATION',
autoGeneration,
})Reducers
Our actions are now defined, so we can move on to reducers. To begin, as with actions, we add a types.js file in the reducers folder.
/*
* @flow
*/
export type SettingsState = {
+passwordLength: number,
+autoGeneration: boolean,
}
export type ReduxState = {
+settings: SettingsState,
}Here we defined the type of the settings state that will be used in the settings reducer, and we also defined the redux state that represents the global state of the application.
The + before each attribute defines them as read-only.
Add the settings.js file, still in the reducers folder:
/*
* @flow
*/
import type { Action } from '../actions/types'
import type { SettingsState } from './types'
const initialState: SettingsState = {
passwordLength: 12,
autoGeneration: false,
}
const settingsState = (state: SettingsState = initialState, action: Action): SettingsState => {
switch (action.type) {
case 'SET_PASSWORD_LENGTH':
return { ...state, passwordLength: action.length }
case 'SET_AUTO_GENERATION':
return { ...state, autoGeneration: action.autoGeneration }
default:
return state
}
}
export default settingsStateIn this reducer, we start by importing the Action type and the SettingsState type, then we define the initial state values, and finally we create the settingsState reducer.
Next, we will create our rootReducer, the one that combines each reducer we will create in the application.
Add an index.js file:
/*
* @flow
*/
import { combineReducers } from 'redux'
import settings from './settings'
const rootReducer = combineReducers({
settings,
})
export default rootReducerStore
Before moving on to integration in the application, all that remains is to add the configureStore.js file in the store folder:
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/es/storage'
import rootReducer from '../reducers'
const config = {
key: 'root',
storage,
}
const reducer = persistReducer(config, rootReducer)
export default function configureStore() {
const middleware = [thunkMiddleware]
if (process.env.NODE_ENV === 'development') {
const loggerMiddleware = createLogger()
middleware.push(loggerMiddleware)
}
const store = compose(applyMiddleware(...middleware))(createStore)(reducer)
const persistor = persistStore(store)
return { persistor, store }
}Here we import all the libraries needed to configure the store, then we define the configuration for redux-persist's persistReducer. If we are in a development environment, we add the logger middleware, then return the store and the persistor.
Setup
First, to keep the index.js file located at the root of src as clear as possible, we will move the StackNavigator code.
In the screens folder, add a StackNavigator.js file and add the following code:
/*
* @flow
*/
import { Platform } from 'react-native'
import { StackNavigator } from 'react-navigation'
import DrawerNavigator from './DrawerNavigator'
import TabNavigator from './TabNavigator'
import strings from '../locales/strings'
import UnlockScreen from './Unlock'
import SetupScreen from './Setup'
import ReadOnlyScreen from './ReadOnly'
import EditScreen from './Edit'
const NestedNav = Platform.OS === 'android' ? DrawerNavigator : TabNavigator
const StackNav = StackNavigator({
Setup: {
screen: SetupScreen,
navigationOptions: () => ({
title: strings.setup,
headerStyle: { backgroundColor: '#01D88D' },
headerTintColor: 'white',
}),
},
Unlock: {
screen: UnlockScreen,
navigationOptions: () => ({
header: null,
}),
},
App: {
screen: NestedNav,
navigationOptions: () => ({
header: null,
}),
},
ReadOnly: {
screen: ReadOnlyScreen,
navigationOptions: ({ navigation }: Object) => ({
title: navigation.state.params.siteName,
headerStyle: { backgroundColor: '#01D88D' },
headerTintColor: 'white',
}),
},
Edit: {
screen: EditScreen,
navigationOptions: ({ navigation }: Object) => ({
header: null,
}),
},
})
export default StackNavWe simply moved the contents of index.js into this file.
Then replace the contents of index.js with this:
import React from 'react'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/lib/integration/react'
import configureStore from './store/configureStore'
import Loader from './components/Loader'
import StackNavigator from './screens/StackNavigator'
const { persistor, store } = configureStore()
const App = () => (
<PersistGate persistor={persistor} loading={<Loader />}>
<Provider store={store}>
<StackNavigator />
</Provider>
</PersistGate>
)
export default AppTo save our application's data, we set up redux-persist. The operating principle is as follows: a copy of the global state is saved through async storage. Then, whenever the application opens, the state is replaced with the copy of the saved data. This step is called rehydration.
Here we use the PersistGate component from redux-persist, which takes care of delaying the display of the components until the store has been rehydrated.
We need to add the Loader component that we use in PersistGate. In the components folder, add Loader.js:
/*
* @flow
*/
import React from 'react'
import { StyleSheet, View } from 'react-native'
import * as Animatable from 'react-native-animatable'
import { WHITE } from '../constants/colors'
const image = require('../img/book.png')
const Loader = () => (
<View style={styles.container}>
<Animatable.Image source={image} animation="pulse" easing="ease-out" iterationCount="infinite" />
</View>
)
export default Loader
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: WHITE,
},
})Finally, all we have left to do is integrate redux with our Settings screen.
First, add the following imports in the Settings.js file in the screens folder:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import type { ReduxState } from '../reducers/types'
import * as SettingsActions from '../actions/settings'Then replace:
export default SettingsScreenwith
function mapStateToProps(state: ReduxState) {
return {
passwordLenght: state.settings.passwordLength,
autoGeneration: state.settings.autoGeneration,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators(SettingsActions, dispatch),
}))(SettingsScreen)Here we connect our screen to Redux. The mapStateToProps function retrieves the entire state of our application and gives us access to the values from the settings state so we can pass them as props to our screen.
Next, we no longer need state at screen level, so we can remove the State type:
type State = {
passwordLength: number,
autoGeneration: boolean,
}and replace it with the following Props type:
type Props = {
passwordLength: number,
autoGeneration: boolean,
navigation: Object,
actions: Object,
}To finish, modify the rest of the screen as follows:
class SettingsScreen extends Component<void, Props, void> {
deleteAllPasswords() {
Alert.alert(strings.clear, strings.clearConfirmation, [
{ text: strings.cancel, style: 'cancel' },
{
text: strings.delete,
onPress: () => console.log('all password delete'),
},
])
}
render() {
return (
<View style={styles.container}>
<NavBar title={strings.settings} actionLeft={() => this.props.navigation.navigate('DrawerOpen')} />
<SliderRow
label={strings.passwordLength}
onValueChange={value => this.props.actions.SetPasswordLength(value)}
selectedValue={this.props.passwordLength}
/>
<CheckBoxRow
label={strings.passwordAuto}
iconName="star"
isChecked={this.props.autoGeneration}
switchValueChange={value => this.props.actions.SetAutoGeneration(value)}
/>
<View style={styles.itemContainer}>
<SettingRow
label={strings.deleteAllPasswords}
iconName="trash"
iconBackground={DELETE_COLOR}
iosOultine
onPress={() => this.deleteAllPasswords()}
/>
</View>
</View>
)
}
}Here, thanks to redux, we replaced all the component state values with props pulled from the redux settings state.
That's it: we are finished adding redux and managing settings.