Skip to content

Commit

Permalink
Merge pull request #264 from Expensify/marcaaron-usePropsNotState
Browse files Browse the repository at this point in the history
Switch to using props instead of state for Ion
  • Loading branch information
tgolen authored Aug 20, 2020
2 parents 9e1bb4a + 5e871d5 commit 4701bdc
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 90 deletions.
17 changes: 16 additions & 1 deletion src/Expensify.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';

// import {Beforeunload} from 'react-beforeunload';
import SignInPage from './page/SignInPage';
Expand All @@ -21,6 +22,17 @@ import {
// Initialize the store when the app loads for the first time
Ion.init();

const propTypes = {
/* Ion Props */

// A route set by Ion that we will redirect to if present. Always empty on app init.
redirectTo: PropTypes.string,
};

const defaultProps = {
redirectTo: '',
};

class Expensify extends Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -62,7 +74,7 @@ class Expensify extends Component {

// We can only have a redirectTo if this is not the initial render so if we have one we'll
// always navigate to it. If we are not authenticated by this point then we'll force navigate to sign in.
const redirectTo = this.state.redirectTo || (!this.state.authToken && '/signin');
const redirectTo = this.props.redirectTo || (!this.state.authToken && '/signin');

return (

Expand All @@ -84,6 +96,9 @@ class Expensify extends Component {
}
}

Expensify.propTypes = propTypes;
Expensify.defaultProps = defaultProps;

export default WithIon({
redirectTo: {
key: IONKEYS.APP_REDIRECT_TO,
Expand Down
7 changes: 3 additions & 4 deletions src/components/WithIon.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,12 @@ export default function (mapIonToState) {
* @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the
* component
* @param {string} statePropertyName the name of the state property that Ion will add the data to
* @param {object} reactComponent a reference to the react component whose state needs updated by Ion
*/
connectMappingToIon(mapping, statePropertyName, reactComponent) {
connectMappingToIon(mapping, statePropertyName) {
const ionConnectionConfig = {
...mapping,
statePropertyName,
reactComponent,
withIonInstance: this,
};

// Connect to Ion and keep track of the connectionID
Expand All @@ -120,7 +119,7 @@ export default function (mapIonToState) {
// Pre-fill the state with any data already in the store
if (mapping.initWithStoredValues !== false) {
Ion.get(ionConnectionConfig.key, mapping.path, mapping.defaultValue)
.then(data => reactComponent.setState({[statePropertyName]: data}));
.then(data => this.setState({[statePropertyName]: data}));
}

// Load the data from an API request if necessary
Expand Down
6 changes: 3 additions & 3 deletions src/lib/Ion.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const callbackToStateMapping = {};
* @param {string} mapping.statePropertyName the name of the property in the state to connect the data to
* @param {boolean} [mapping.addAsCollection] rather than setting a single state value, this will add things to an array
* @param {string} [mapping.collectionID] the name of the ID property to use for the collection
* @param {object} mapping.reactComponent whose setState() method will be called with any changed data
* @param {object} mapping.withIonInstance whose setState() method will be called with any changed data
* @returns {number} an ID to use when calling disconnect
*/
function connect(mapping) {
Expand Down Expand Up @@ -75,7 +75,7 @@ function keyChanged(key, data) {
// Set the state of the react component with either the pathed data, or the data
if (mappedComponent.addAsCollection) {
// Add the data to an array of existing items
mappedComponent.reactComponent.setState((prevState) => {
mappedComponent.withIonInstance.setState((prevState) => {
const collection = prevState[mappedComponent.statePropertyName] || {};
collection[newValue[mappedComponent.collectionID]] = newValue;
const newState = {
Expand All @@ -84,7 +84,7 @@ function keyChanged(key, data) {
return newState;
});
} else {
mappedComponent.reactComponent.setState({
mappedComponent.withIonInstance.setState({
[mappedComponent.statePropertyName]: newValue,
});
}
Expand Down
64 changes: 36 additions & 28 deletions src/page/HomePage/HeaderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,47 @@ const propTypes = {

// Decides whether we should show the hamburger menu button
shouldShowHamburgerButton: PropTypes.bool.isRequired,

/* Ion Props */

// Name of the report (if we have one)
reportName: PropTypes.string,
};

class HeaderView extends React.Component {
render() {
return (
<View style={[styles.appContentHeader]}>
<View style={[styles.appContentHeaderTitle]}>
{this.props.shouldShowHamburgerButton && (
<TouchableOpacity
onPress={this.props.onHamburgerButtonClicked}
style={[styles.LHNToggle]}
>
<Image
resizeMode="contain"
style={[styles.LHNToggleIcon]}
source={LHNToggle}
/>
</TouchableOpacity>
)}
{this.state && this.state.reportName && (
<Text numberOfLines={2} style={[styles.navText]}>
{this.state.reportName}
</Text>
)}
</View>
</View>
);
}
}
const defaultProps = {
reportName: '',
};

const HeaderView = props => (
<View style={[styles.appContentHeader]}>
<View style={[styles.appContentHeaderTitle]}>
{props.shouldShowHamburgerButton && (
<TouchableOpacity
onPress={props.onHamburgerButtonClicked}
style={[styles.LHNToggle]}
>
<Image
resizeMode="contain"
style={[styles.LHNToggleIcon]}
source={LHNToggle}
/>
</TouchableOpacity>
)}
{props.reportName && (
<Text numberOfLines={2} style={[styles.navText]}>
{props.reportName}
</Text>
)}
</View>
</View>
);

HeaderView.propTypes = propTypes;
HeaderView.displayName = 'HeaderView';
HeaderView.defaultProps = defaultProps;

export default withRouter(WithIon({
// Map this.state.reportName to the data for a specific report in the store, and bind it to the reportName property
// Map this.props.reportName to the data for a specific report in the store, and bind it to the reportName property
// It uses the data returned from the props path (ie. the reportID) to replace %DATAFROMPROPS% in the key it
// binds to
reportName: {
Expand Down
24 changes: 15 additions & 9 deletions src/page/HomePage/MainView.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@ const propTypes = {
// This comes from withRouter
// eslint-disable-next-line react/forbid-prop-types
match: PropTypes.object.isRequired,
};

class MainView extends React.Component {
constructor(props) {
super(props);
/* Ion Props */

this.state = {};
}
// List of reports to display
reports: PropTypes.arrayOf(PropTypes.shape({
reportID: PropTypes.number,
})),
};

const defaultProps = {
reports: [],
};

class MainView extends React.Component {
render() {
if (!this.state || !this.state.reports || this.state.reports.length === 0) {
if (this.props.reports.length === 0) {
return null;
}

const reportIDInURL = parseInt(this.props.match.params.reportID, 10);

// The styles for each of our reports. Basically, they are all hidden except for the one matching the
// reportID in the URL
const reportStyles = _.reduce(this.state.reports, (memo, report) => {
const reportStyles = _.reduce(this.props.reports, (memo, report) => {
const finalData = {...memo};
const reportStyle = reportIDInURL === report.reportID
? [styles.dFlex, styles.flex1]
Expand All @@ -41,7 +46,7 @@ class MainView extends React.Component {

return (
<>
{_.map(this.state.reports, report => (
{_.map(this.props.reports, report => (
<View
key={report.reportID}
style={reportStyles[report.reportID]}
Expand All @@ -55,6 +60,7 @@ class MainView extends React.Component {
}

MainView.propTypes = propTypes;
MainView.defaultProps = defaultProps;

export default withRouter(WithIon({
reports: {
Expand Down
29 changes: 20 additions & 9 deletions src/page/HomePage/Report/ReportHistoryView.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,24 @@ import IONKEYS from '../../../IONKEYS';
import ReportHistoryItem from './ReportHistoryItem';
import styles from '../../../style/StyleSheet';
import {withRouter} from '../../../lib/Router';
import ReportHistoryPropsTypes from './ReportHistoryPropsTypes';

const propTypes = {
// The ID of the report actions will be created for
reportID: PropTypes.number.isRequired,

/* Ion Props */

// Array of report history items for this report
reportHistory: PropTypes.arrayOf(PropTypes.shape(ReportHistoryPropsTypes)),

// Current user authToken
authToken: PropTypes.string,
};

const defaultProps = {
reportHistory: [],
authToken: '',
};

class ReportHistoryView extends React.Component {
Expand Down Expand Up @@ -71,7 +85,7 @@ class ReportHistoryView extends React.Component {
* action when scrolled
*/
recordMaxAction() {
const reportHistory = lodashGet(this.state, 'reportHistory', []);
const reportHistory = lodashGet(this.props, 'reportHistory', []);
const maxVisibleSequenceNumber = _.chain(reportHistory)
.pluck('sequenceNumber')
.max()
Expand Down Expand Up @@ -113,12 +127,7 @@ class ReportHistoryView extends React.Component {
}

render() {
let reportHistory = {};
if (this.state && this.state.reportHistory) {
reportHistory = this.state.reportHistory;
}

if (reportHistory.length === 0) {
if (this.props.reportHistory.length === 0) {
return (
<View style={[styles.chatContent, styles.chatContentEmpty]}>
<Text style={[styles.textP]}>Be the first person to comment!</Text>
Expand All @@ -137,19 +146,21 @@ class ReportHistoryView extends React.Component {
paddingVertical: 8
}}
>
{_.chain(reportHistory).sortBy('sequenceNumber').map((item, index) => (
{_.chain(this.props.reportHistory).sortBy('sequenceNumber').map((item, index) => (
<ReportHistoryItem
key={item.sequenceNumber}
historyItem={item}
authToken={this.state.authToken}
authToken={this.props.authToken}
displayAsGroup={this.isConsecutiveHistoryItemMadeByPreviousActor(index)}
/>
)).value()}
</ScrollView>
);
}
}

ReportHistoryView.propTypes = propTypes;
ReportHistoryView.defaultProps = defaultProps;

const key = `${IONKEYS.REPORT_HISTORY}_%DATAFROMPROPS%`;
export default withRouter(WithIon({
Expand Down
54 changes: 32 additions & 22 deletions src/page/HomePage/SidebarLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,41 @@ const propTypes = {

// Toggles the hamburger menu open and closed
onLinkClick: PropTypes.func.isRequired,

/* Ion Props */

// Does the report for this link have unread comments?
isUnread: PropTypes.bool,
};

class SidebarLink extends React.Component {
render() {
const paramsReportID = parseInt(this.props.match.params.reportID, 10);
const isReportActive = paramsReportID === this.props.reportID;
const linkWrapperActiveStyle = isReportActive && styles.sidebarLinkWrapperActive;
const linkActiveStyle = isReportActive ? styles.sidebarLinkActive : styles.sidebarLink;
const textActiveStyle = isReportActive ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textActiveUnreadStyle = this.state && this.state.isUnread
? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle];
return (
<View style={[styles.sidebarListItem, linkWrapperActiveStyle]}>
<PressableLink onClick={this.props.onLinkClick} to={`/${this.props.reportID}`} style={linkActiveStyle}>
<View style={[styles.sidebarLinkInner]}>
<Text numberOfLines={1} style={textActiveUnreadStyle}>
{this.props.reportName}
</Text>
</View>
</PressableLink>
</View>
);
}
}
const defaultProps = {
isUnread: false,
};

const SidebarLink = (props) => {
const paramsReportID = parseInt(props.match.params.reportID, 10);
const isReportActive = paramsReportID === props.reportID;
const linkWrapperActiveStyle = isReportActive && styles.sidebarLinkWrapperActive;
const linkActiveStyle = isReportActive ? styles.sidebarLinkActive : styles.sidebarLink;
const textActiveStyle = isReportActive ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textActiveUnreadStyle = props.isUnread
? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle];
return (
<View style={[styles.sidebarListItem, linkWrapperActiveStyle]}>
<PressableLink onClick={props.onLinkClick} to={`/${props.reportID}`} style={linkActiveStyle}>
<View style={[styles.sidebarLinkInner]}>
<Text numberOfLines={1} style={textActiveUnreadStyle}>
{props.reportName}
</Text>
</View>
</PressableLink>
</View>
);
};

SidebarLink.displayName = 'SidebarLink';
SidebarLink.propTypes = propTypes;
SidebarLink.defaultProps = defaultProps;

export default withRouter(WithIon({
isUnread: {
Expand Down
Loading

0 comments on commit 4701bdc

Please sign in to comment.