Integrating Redux In Settings
Settings page in SUSI.AI earlier implemented using Flux needed to be migrated to Redux, Redux integration eases handling of global state management. With Redux being integrated with Settings, the code could be split into smaller manageable components. Earlier implementation involved passing data to other Settings Tab and using Settings component as a state source. This resulted in Settings component getting more lines of code(~1300 LOC). After using Redux, only ~280 LOC is present in settings component file, and separate files for each settings tab view.
Code Integration
Once user login, the App component fetches User Settings in ComponentDidMount.
componentDidMount = () => { const { accessToken, actions } = this.props; window.addEventListener('offline', this.onUserOffline); window.addEventListener('online', this.onUserOnline); actions.getApiKeys(); if (accessToken) { actions.getAdmin(); actions.getUserSettings().catch(e => { console.log(e); }); } };
The action getUserSettings fetch users settings from apis.getUserSettings and populates the store settings using SETTINGS_GET_USER_SETTINGS reducer function.
Default States:
The store consists of all the settings required for persisting data.
const defaultState = { theme: 'light', server: 'https://api.susi.ai', enterAsSend: true, micInput: true, speechOutput: true, speechOutputAlways: false, speechRate: 1, speechPitch: 1, ttsLanguage: 'en-US', userName: '', prefLanguage: 'en-US', timeZone: 'UTC-02', customThemeValue: { header: '#4285f4', pane: '#f3f2f4', body: '#fff', composer: '#f3f2f4', textarea: '#fff', button: '#4285f4', }, localStorage: true, countryCode: 'US', countryDialCode: '+1', phoneNo: '', checked: false, serverUrl: 'https://api.susi.ai', backgroundImage: '', messageBackgroundImage: '', avatarType: 'default', devices: {}, };
Reducer action:
SETTINGS_GET_USER_SETTINGS sets the settings store from API payload. The customThemeValue is an object, which is populated once the settings are fetched and the string is split into array.
[actionTypes.SETTINGS_GET_USER_SETTINGS](state, { error, payload }) { const { settings, devices = {} } = payload; if (error || !settings) { return state; } const { theme = defaultState.theme, server, …. avatarType, } = settings; let { customThemeValue } = settings; const themeArray = customThemeValue ? customThemeValue.split(',').map(value => `#${value}`) : defaultState.customThemeValue; return { ...state, devices, server, serverUrl, theme, enterAsSend: enterAsSend === 'true', micInput: micInput === 'true', speechOutput: speechOutput === 'true', speechOutputAlways: speechOutputAlways === 'true', speechRate: Number(speechRate), speechPitch: Number(speechPitch), ttsLanguage, userName, prefLanguage, timeZone, countryCode, countryDialCode, phoneNo, checked: checked === 'true', backgroundImage, messageBackgroundImage, avatarType, customThemeValue: { header: themeArray[0], pane: themeArray[1], body: themeArray[2], composer: themeArray[3], textarea: themeArray[4], button: themeArray[5], }, }; },
Updating Settings
Reduce function:
The payload contains the updated state, the reducer returns a new state with payload appended.
setUserSettings: createAction(
actionTypes.SETTINGS_SET_USER_SETTINGS,
returnArgumentsFn,
)
Action:
The setUserSettings action updates the settings store, once the user presses the Save Button in the Settings Tab.
function mapStateToProps(store) { return { enterAsSend: store.settings.enterAsSend, }; }
Redux integration in the component
Redux is used in ChatAppTab as: The local state of ChatAppTab is initialized with redux store state. Using mapStateToProps we can access enterAsSend object from the redux store.
The connect function connects the React component with Redux store and injects props and actions into our component using HOC pattern. It injects data from the store and functions for dispatching actions to store.
Injecting props in local components:
function mapStateToProps(store) { return { enterAsSend: store.settings.enterAsSend, }; }
components/Settings/ChatAppTab.react.js
Once a user makes a change in the tab and saves it, we need to dispatch an action to modify our prop enterAsSend inside the redux store.
function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ ...settingActions, ...uiActions }, dispatch), }; }
components/Settings/ChatAppTab.react.j
handleSubmit = () => { const { actions } = this.props; const { enterAsSend } = this.state; this.setState({ loading: true }); setUserSettings({ enterAsSend }) .then(data => { if (data.accepted) { actions.openSnackBar({ snackBarMessage: 'Settings updated', }); actions.setUserSettings({ enterAsSend }); this.setState({ loading: false }); } else { actions.openSnackBar({ snackBarMessage: 'Failed to save Settings', }); this.setState({ loading: false }); } }) .catch(error => { actions.openSnackBar({ snackBarMessage: 'Failed to save Settings', }); }); };
components/Settings/ChatAppTab.react.js
handleSubmit is invoked when user presses the Save Button, action.setUserSettings is dispatched once API returns accepted: true response.
To conclude, the removal of flux and integration of redux gave the advantage of making the code more modular and separate the logic into smaller files. In the future, more settings can be easily added using the same data flow pattern.
Resources
- Migrating to Redux from flux: https://redux.js.org/recipes/migrating-to-redux
- Why use Redux- https://blog.logrocket.com/why-use-redux-reasons-with-clear-examples-d21bffd5835/
- Documentation of Redux – https://redux.js.org/
- React Anti-Pattern: Prop-Drilling – https://codeburst.io/react-anti-pattern-prop-drilling-54474d5236bd
- Redux connect – https://react-redux.js.org/api/connect
- Link to the PR: https://github.com/fossasia/susi.ai/pull/2129
Tags
SUSI.AI, FOSSASIA, GSoC19, Redux, SUSI Chat, Settings