React Native Gestionnaire de mots de passe : Redux & Gestion des réglages

30 octobre 2017

Thibault MOCELLIN
React Native Gestionnaire de mots de passe : Redux & Gestion des réglages

Bonjour à tous aujourd'hui dans cet article nous allons nous occuper de la gestion des paramètres de l'application. Nous allons donc commencer par ajouter Redux et pendant que nous le configurerons nous allons créer toutes les briques nécessaires à la gestion des paramètres (reducers/actions/etc..). Enfin pour terminer nous intégrerons tous ça à notre écran Réglage.

Installation de Redux

La première chose à faire est donc d'installer redux et les autre librairies liées :

yarn add redux react-redux redux-logger redux-persist redux-thunk

Ensuite on met en place l'arborescance des dossiers pour les briques de redux :

cd src
mkdir actions reducers store

Actions

Maintenant que Redux est installé nous allons nous occuper des actions. Puisque nous utilisons Flow nous commençons par créer un fichier types.js dans le dossier actions.

Ce fichier contiendra les différents types de nos 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 | SetAutoGenerationAction

Ici premièrement nous créons deux types Flow qui sont SetPasswordLengthAction et SetAutoGenerationAction chacun représente une action, nous retrouvons donc pour chacune le type ainsi que la valeur qui sera retournée par l'action.

Ensuite nous définissons un type Action qui sera le type des actions de chaque reducer. Ce type Action peut être de type SetPasswordLengthAction ou SetAutoGenerationAction nous définissons cela grace à la notation de |.

Nous allons créer le fichier settings.js dans le dossier actions qui lui contiendra les actions et les actionCreators pour la partie Réglage :

/*
* @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

Nos actions sont maintenant définies nous allons pouvoir passer au reducers. Pour commencer comme pour les actions nous allons ajouter un fichier types.js dans le dossier reducers.

/*
* @flow
*/

export type SettingsState = {
  +passwordLength: number,
  +autoGeneration: boolean,
}

export type ReduxState = {
  +settings: SettingsState,
}

Ici nous avons défini le type du state settings qui sera utilisé dans le reducer settings et nous avons aussi défini le state redux qui représente le state globale de l'application.

Le + devant chaque attribut permet de définir qu'ils sont readOnly.

Ajoutez le fichier settings.js toujours dans le dossier reducers :

/*
* @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 settingsState

Dans ce reducer nous avons commencer par importer le type Action et le type SettingsState puis on définit les valeurs initiales du state enfin on créé le reducer settingsState.

Ensuite nous allons créer notre rootReducer celui qui combine chacun des reducers que nous allons créer dans l'application.

Ajoutez un fichier index.js :

/*
* @flow
*/

import { combineReducers } from 'redux'
import settings from './settings'

const rootReducer = combineReducers({
  settings,
})

export default rootReducer

Store

Avant de passer à l'intégration dans l'application il ne nous reste plus qu'a ajouter le fichier configureStore.js dans le dossier store :

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 }
}

Ici nous importons l'ensembles des librairies nécéssaire à la configuration du store puis on définit la configuration pour le persitReducer de redux-persist. On ajoute si nous somme en environnement de développement le logger middleware puis on retourne le store et le persistor.

Mise en place

Premièrement pour que le fichier index.js située à la racine de src soit le plus claire possible nous allons déplacer le code du StackNavigator.

Dans le dossier screens ajoutez un fichier StackNavigator.js et ajoutez le code suivant :

/*
* @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 StackNav

Nous avons simplement déplacer le contenu d'index.js dans ce fichier.

Ensuite remplacer le contenu d'index.js par ceci :

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 App

Afin de pouvoir sauvegarder les données de notre application nous avons mis en place redux-persit le principe de fonctionnement est le suivant une copie du state globale est enregistrer grace à l'async storage. Ensuite dès que l'on ouvre l'application le state est remplacer par la copie des données enregistrées cette étape est appelée la réhydratation.

Ici on utilise le composant PersitGate de redux-persist qui va s'occuper pour nous de différez l'affichage des composants le temp que le store soit réhydraté.

Il faut que l'on ajoute le composant Loader que nous utilisons dans le PersitGate. Dans le dossier components rajouter 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,
  },
})

Enfin nous avons plus qu'a intégrer redux avec notre écran Setting :

Tout d'abord rajoutez les imports suivants dans le fichier Settings.js du dossier screens :

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import type { ReduxState } from '../reducers/types'
import * as SettingsActions from '../actions/settings'

Ensuite remplacez :

export default SettingsScreen

par

function mapStateToProps(state: ReduxState) {
  return {
    passwordLenght: state.settings.passwordLength,
    autoGeneration: state.settings.autoGeneration,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators(SettingsActions, dispatch),
}))(SettingsScreen)

Ici nous connectons notre écran à Redux. La fonction mapStateToProps récupère l'ensemble du state de notre application et nous permet d'avoir accès aux valeurs du state settings et de les passer en tant que propriétés pour notre écran.

Ensuite nous n'avons plus besoin de state au niveau de l'écran, on peut donc supprimer le type State :

type State = {
  passwordLength: number,
  autoGeneration: boolean,
}

et le remplacer par le type Props suivant :

type Props = {
  passwordLength: number,
  autoGeneration: boolean,
  navigation: Object,
  actions: Object,
}

Pour terminer modifiez le reste de l'écran de la manière suivante :

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>
    )
  }
}

Ici grace à redux nous avons remplacé toutes les valeurs de state du composant par les propriétés tirées du state settings de redux.

Voila nous avons fini avec l'ajout de redux et la gestion des paramètres.

Retrouvez le code source ici.