Skip to content

Commit

Permalink
Added an example for components connected to Redux state
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor committed Sep 10, 2017
1 parent 140c33f commit 3dd15a0
Show file tree
Hide file tree
Showing 28 changed files with 186 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Project
web/js/bundle.*

# Node
logs
*.log
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
React Native Code Reuse
=======================
This example shows how to share code between different platforms when using React & React Native. It is based on the fact that React Native will detect when a file has a .ios. or .android. extension and load the relevant platform file when required from other components (see [Platform-specific extensions](https://facebook.github.io/react-native/docs/platform-specific-code.html#platform-specific-extensions)).
This example shows how to share code between different platforms (web, iOS and Android) when using React & React Native. It is based on the fact that React Native will detect when a file has a .ios. or .android. extension and load the relevant platform file when required from other components (see [Platform-specific extensions](https://facebook.github.io/react-native/docs/platform-specific-code.html#platform-specific-extensions)).

Each platform uses the corresponding ```index.js``` file as an entry point. All other code resides in the [app](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app) directory.

Each component is presented as a subpackage containing implementations for all platforms. For simple components which don't do any logic, we define only two views for web and mobile and load the relevant view in the subpackage index. Like in the [App](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app/components/App) component.

For components which have a different appearance for iOS and Android, we define two views with ```ios.js``` and ```android.js``` extensions each containing a platform specific code. Like in the [Title](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app/components/Title) component.

And for components which do some logic, we also add a component container to define that logic. In such case in the subpackage index, we load the container and it renders the view. For max code reuse, we can put all shared logic into the abstract container and put platform specific code into subclasses. Like in the [HelpButton](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app/components/HelpButton) component.
For components which do some logic, we add a container component to define that logic. In such case in the subpackage index, we load the container and it renders the view. For max code reuse, we can put all shared logic into the abstract container and put platform specific code into subclasses. Like in the [AboutButton](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app/components/AboutButton) component.

For components which need to use Redux state and dispatch actions (connected components), we connect them once in the subpackage index and pass the received props to containers or views the same as previous components. You can check the example in the [HelpButton](https://github.com/ihor/ReactNativeCodeReuseExample/tree/master/app/components/HelpButton) component.

Installation
============
Expand Down
7 changes: 7 additions & 0 deletions app/actions/help-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as types from './types';

export const helpRequested = () => {
return {
type: types.HELP_REQUESTED
};
};
1 change: 1 addition & 0 deletions app/actions/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const HELP_REQUESTED = 'HELP_REQUESTED';
8 changes: 8 additions & 0 deletions app/components/AboutButton/AboutButtonContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import AbstractAboutButtonContainer from './AbstractAboutButtonContainer';

export default class AboutButtonContainer extends AbstractAboutButtonContainer {
onClick() {
alert('This is an example application to show how to reuse code between React and React Native');
}
}
10 changes: 10 additions & 0 deletions app/components/AboutButton/AboutButtonContainer.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { Alert } from 'react-native';

import AbstractAboutButtonContainer from './AbstractAboutButtonContainer';

export default class AboutButtonContainer extends AbstractAboutButtonContainer {
onClick() {
Alert.alert('This is an example application to show how to reuse code between React and React Native');
}
}
6 changes: 6 additions & 0 deletions app/components/AboutButton/AboutButtonView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';

export default props =>
<button className="button" onClick={props.onClick}>
About
</button>;
12 changes: 12 additions & 0 deletions app/components/AboutButton/AboutButtonView.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import Button from 'react-native-button';

import styles from '../../native/styles';

export default props =>
<Button
onPress={props.onClick}
style={styles.buttonText}
containerStyle={styles.button}>
About
</Button>;
23 changes: 23 additions & 0 deletions app/components/AboutButton/AbstractAboutButtonContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import AboutButtonView from './AboutButtonView';

export default class AbstractAboutButtonContainer extends React.Component {
constructor(props) {
// new.target is not working on Android
// if (new.target === AbstractAboutButtonContainer) {
// throw new TypeError('Cannot construct AbstractAboutButtonContainer instances directly');
// }

super(props);

this.onClick = this.onClick.bind(this);
}

onClick() {
throw new TypeError('Abstract method onClick is not implemented');
}

render() {
return <AboutButtonView onClick={this.onClick}/>;
}
}
2 changes: 2 additions & 0 deletions app/components/AboutButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import AboutButtonContainer from './AboutButtonContainer';
export default AboutButtonContainer;
2 changes: 2 additions & 0 deletions app/components/App/AppView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';

import Title from '../Title';
import AboutButton from '../AboutButton';
import HelpButton from '../HelpButton';

export default () =>
<div className="container">
<Title/>
<AboutButton/>
<HelpButton/>
</div>;
6 changes: 5 additions & 1 deletion app/components/App/AppView.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { View } from 'react-native';
import styles from '../../native/styles';

import Title from '../Title';
import AboutButton from '../AboutButton';
import HelpButton from '../HelpButton';

export default () =>
<View style={styles.container}>
<Title/>
<HelpButton/>
<View style={styles.buttonContainer}>
<AboutButton/>
<HelpButton/>
</View>
</View>;
15 changes: 14 additions & 1 deletion app/components/App/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
import React from 'react';
import { Provider } from 'react-redux';

import store from '../../store';
import AppView from './AppView';
export default AppView;

export default class AppContainer extends React.Component {
render() {
return (
<Provider store={store}>
<AppView/>
</Provider>
)
}
}
16 changes: 14 additions & 2 deletions app/components/HelpButton/AbstractHelpButtonContainer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';

import HelpButtonView from './HelpButtonView';

export default class AbstractHelpButtonContainer extends React.Component {
Expand All @@ -14,10 +16,20 @@ export default class AbstractHelpButtonContainer extends React.Component {
}

onClick() {
throw new TypeError('Abstract method onClick is not implemented');
this.displayMessage(`You asked for help ${this.props.helpRequests + 1} time(s)`);
this.props.helpRequested();
}

displayMessage(message) {
throw new TypeError('Abstract method displayMessage is not implemented');
}

render() {
return <HelpButtonView onClick={this.onClick}/>;
}
}
}

AbstractHelpButtonContainer.propTypes = {
helpRequests: PropTypes.number.isRequired,
helpRequested: PropTypes.func.isRequired,
};
4 changes: 2 additions & 2 deletions app/components/HelpButton/HelpButtonContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import AbstractHelpButtonContainer from './AbstractHelpButtonContainer';

export default class HelpButtonContainer extends AbstractHelpButtonContainer {
onClick() {
alert('How can we help you?');
displayMessage(message) {
alert(message);
}
}
4 changes: 2 additions & 2 deletions app/components/HelpButton/HelpButtonContainer.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Alert } from 'react-native';
import AbstractHelpButtonContainer from './AbstractHelpButtonContainer';

export default class HelpButtonContainer extends AbstractHelpButtonContainer {
onClick() {
Alert.alert('How can we help you?');
displayMessage(message) {
Alert.alert(message);
}
}
2 changes: 1 addition & 1 deletion app/components/HelpButton/HelpButtonView.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

export default props =>
<button className="help-button" onClick={props.onClick}>
<button className="button" onClick={props.onClick}>
Help
</button>;
4 changes: 2 additions & 2 deletions app/components/HelpButton/HelpButtonView.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styles from '../../native/styles';
export default props =>
<Button
onPress={props.onClick}
style={styles.helpButtonText}
containerStyle={styles.helpButton}>
style={styles.buttonText}
containerStyle={styles.button}>
Help
</Button>;
26 changes: 25 additions & 1 deletion app/components/HelpButton/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { helpRequested } from '../../actions/help-actions'
import { getHelpRequestsNumber } from '../../reducers';
import HelpButtonContainer from './HelpButtonContainer';
export default HelpButtonContainer;

class HelpButton extends React.Component {
render() {
return (
<HelpButtonContainer { ...this.props }/>
)
}
}

HelpButton.propTypes = {
helpRequests: PropTypes.number.isRequired,
helpRequested: PropTypes.func.isRequired,
};

const mapStateToProps = store => ({
helpRequests: getHelpRequestsNumber(store),
});

export default connect(mapStateToProps, { helpRequested })(HelpButton)
12 changes: 8 additions & 4 deletions app/native/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginBottom: 20
},
helpButton: {
buttonContainer: {
flexDirection: 'row',
},
button: {
backgroundColor: '#80CBC4',
width: 100,
padding: 10
padding: 10,
margin: 5,
},
helpButtonText: {
buttonText: {
color: '#FFFFFF'
}
},
});

export default styles;
16 changes: 16 additions & 0 deletions app/reducers/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as types from '../actions/types';

const initialState = {
requests: 0,
};

export default function(state = initialState, action) {
switch (action.type) {
case types.HELP_REQUESTED:
return {requests: state.requests + 1};

default: return state;
}
};

export const getRequestsNumber = state => state.requests;
9 changes: 9 additions & 0 deletions app/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { combineReducers } from 'redux';

import help, * as helpSelectors from './help';

export default combineReducers({
help,
});

export const getHelpRequestsNumber = store => helpSelectors.getRequestsNumber(store.help);
4 changes: 4 additions & 0 deletions app/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createStore } from 'redux';
import reducers from './reducers';

export default createStore(reducers);
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
"test": "jest"
},
"dependencies": {
"prop-types": "^15.5.10",
"react": "15.4.2",
"react-dom": "15.4",
"react-native": "0.41.2",
"react-native-button": "^1.7.1"
"react-native-button": "^1.7.1",
"react-redux": "^5.0.6",
"redux": "^3.7.2"
},
"devDependencies": {
"babel-cli": "^6.22",
Expand Down
Binary file modified screenshots/react-native-code-reuse-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/react-native-code-reuse-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion web/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ body {
margin-bottom: 32px;
}

.help-button {
.button {
border: 0;
background: #80CBC4;
width: 180px;
height: 40px;
margin: 5px;
color: #FFFFFF;
font-size: 18px;
text-transform: uppercase;
Expand Down
Empty file added web/js/.gitkeep
Empty file.

0 comments on commit 3dd15a0

Please sign in to comment.