diff --git a/README.md b/README.md deleted file mode 100644 index 41e88a5..0000000 --- a/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# react_on_rails Generator Results - -You can find examples of running the generators from [react_on_rails](https://github.com/shakacode/react_on_rails/). - -For each release, we'll be publishing a set of pull requests that show the various permutations of running the generator on top of a brand new Rails 4 App (`rails new my_app`). - -Run `rails generate react_on_rails:install --help` for descriptions of all available options: - -``` -Usage: - rails generate react_on_rails:install [options] - -Options: - -R, [--redux], [--no-redux] # Install Redux gems and Redux version of Hello World Example - -S, [--server-rendering], [--no-server-rendering] # Add necessary files and configurations for server-side rendering - -j, [--skip-js-linters], [--no-skip-js-linters] # Skip installing JavaScript linting files - -L, [--ruby-linters], [--no-ruby-linters] # Install ruby linting files, tasks, and configs - -H, [--heroku-deployment], [--no-heroku-deployment] # Install files necessary for deploying to Heroku - -Runtime options: - -f, [--force] # Overwrite files that already exist - -p, [--pretend], [--no-pretend] # Run but do not make any changes - -q, [--quiet], [--no-quiet] # Suppress status output - -s, [--skip], [--no-skip] # Skip files that already exist - -Description: - Create react on rails files for install generator. -``` - -See the [react_on_rails README.md](https://github.com/shakacode/react_on_rails/blob/master/README.md) for more information. - -Note: We support [react-router](https://github.com/rackt/react-router/), even with server rendering, but we have not yet created generator options for that. - -# v1.1.1 - -* [basic](https://github.com/shakacode/react_on_rails-generator-results/pull/121) -* [basic-server-rendering](https://github.com/shakacode/react_on_rails-generator-results/pull/122) -* [redux](https://github.com/shakacode/react_on_rails-generator-results/pull/123) -* [redux-server-rendering](https://github.com/shakacode/react_on_rails-generator-results/pull/124) -* [basic-heroku-deployment](https://github.com/shakacode/react_on_rails-generator-results/pull/125) -* [basic-server-rendering](https://github.com/shakacode/react_on_rails-generator-results/pull/126) -* [redux to redux-server-rendering](https://github.com/shakacode/react_on_rails-generator-results/pull/127) -* [basic to redux](https://github.com/shakacode/react_on_rails-generator-results/pull/129) -* [basic-server-rendering to redux-server-rendering](https://github.com/shakacode/react_on_rails-generator-results/pull/128) -* [basic to basic-heroku-deployment](https://github.com/shakacode/react_on_rails-generator-results/pull/130) diff --git a/bin/rails b/bin/rails index 62687aa..0138d79 100755 --- a/bin/rails +++ b/bin/rails @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) -rescue LoadError +rescue LoadError => e + raise unless e.message.include?('spring') end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' diff --git a/bin/rake b/bin/rake index 2e9ece4..d87d5f5 100755 --- a/bin/rake +++ b/bin/rake @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) -rescue LoadError +rescue LoadError => e + raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' diff --git a/client/app/bundles/HelloWorld/actions/helloWorldActionCreators.jsx b/client/app/bundles/HelloWorld/actions/helloWorldActionCreators.jsx new file mode 100644 index 0000000..f2b884b --- /dev/null +++ b/client/app/bundles/HelloWorld/actions/helloWorldActionCreators.jsx @@ -0,0 +1,8 @@ +import * as actionTypes from '../constants/helloWorldConstants'; + +export function updateName(name) { + return { + type: actionTypes.HELLO_WORLD_NAME_UPDATE, + name, + }; +} diff --git a/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx b/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx index b6916f8..645fe09 100644 --- a/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx +++ b/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx @@ -1,4 +1,8 @@ +// HelloWorldWidget is an arbitrary name for any "dumb" component. We do not recommend suffixing all your +// dump component names with Widget. + import React, { PropTypes } from 'react'; +import Immutable from 'immutable'; import _ from 'lodash'; // Simple example of a React "dumb" component @@ -13,25 +17,30 @@ export default class HelloWorldWidget extends React.Component { } static propTypes = { - name: PropTypes.string.isRequired, - _updateName: PropTypes.func.isRequired, - }; + // We prefix all property and variable names pointing to Immutable.js objects with '$$'. + // This allows us to immediately know we don't call $$helloWorldStore['someProperty'], but instead use + // the Immutable.js `get` API for Immutable.Map + actions: PropTypes.object.isRequired, + $$helloWorldStore: PropTypes.instanceOf(Immutable.Map).isRequired, + } // React will automatically provide us with the event `e` _handleChange(e) { const name = e.target.value; - this.props._updateName(name); + this.props.actions.updateName(name); } render() { + const $$helloWorldStore = this.props.$$helloWorldStore; + const name = $$helloWorldStore.get('name'); return (

- Hello, {this.props.name}! + Hello, {name}!

Say hello to: - +

); diff --git a/client/app/bundles/HelloWorld/constants/helloWorldConstants.jsx b/client/app/bundles/HelloWorld/constants/helloWorldConstants.jsx new file mode 100644 index 0000000..2b5a011 --- /dev/null +++ b/client/app/bundles/HelloWorld/constants/helloWorldConstants.jsx @@ -0,0 +1,8 @@ +// See https://www.npmjs.com/package/mirror-creator +// Allows us to easily setup constants inside of +// client/app/bundles/HelloWorld/actions/helloWorldActionCreators.jsx +import mirrorCreator from 'mirror-creator'; + +export default mirrorCreator([ + 'HELLO_WORLD_NAME_UPDATE', +]); diff --git a/client/app/bundles/HelloWorld/containers/HelloWorld.jsx b/client/app/bundles/HelloWorld/containers/HelloWorld.jsx index a09a037..82e8c8d 100644 --- a/client/app/bundles/HelloWorld/containers/HelloWorld.jsx +++ b/client/app/bundles/HelloWorld/containers/HelloWorld.jsx @@ -1,33 +1,43 @@ import React, { PropTypes } from 'react'; import HelloWorldWidget from '../components/HelloWorldWidget'; -import _ from 'lodash'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import Immutable from 'immutable'; +import * as helloWorldActionCreators from '../actions/helloWorldActionCreators'; + +function select(state) { + // Which part of the Redux global state does our component want to receive as props? + // Note the use of `$$` to prefix the property name because the value is of type Immutable.js + return { $$helloWorldStore: state.$$helloWorldStore }; +} // Simple example of a React "smart" component -export default class HelloWorld extends React.Component { +class HelloWorld extends React.Component { constructor(props, context) { super(props, context); - - // Uses lodash to bind all methods to the context of the object instance, otherwise - // the methods defined here would not refer to the component's class, not the component - // instance itself. - _.bindAll(this, '_updateName'); } static propTypes = { - name: PropTypes.string.isRequired, // this is passed from the Rails view - } + dispatch: PropTypes.func.isRequired, - state = {name: this.props.name} // how to set initial state in es2015 class syntax - - _updateName(name) { - this.setState({name: name}); + // This corresponds to the value used in function select above. + $$helloWorldStore: PropTypes.instanceOf(Immutable.Map).isRequired, } render() { + const { dispatch, $$helloWorldStore } = this.props; + const actions = bindActionCreators(helloWorldActionCreators, dispatch); + + // This uses the ES2015 spread operator to pass properties as it is more DRY + // This is equivalent to: + // return ( -
- -
+ ); } } + +// Don't forget to actually use connect! +// Note that we don't export HelloWorld, but the redux "connected" version of it. +// See https://github.com/rackt/react-redux/blob/master/docs/api.md#examples +export default connect(select)(HelloWorld); diff --git a/client/app/bundles/HelloWorld/reducers/helloWorldReducer.jsx b/client/app/bundles/HelloWorld/reducers/helloWorldReducer.jsx new file mode 100644 index 0000000..df77e65 --- /dev/null +++ b/client/app/bundles/HelloWorld/reducers/helloWorldReducer.jsx @@ -0,0 +1,21 @@ +import Immutable from 'immutable'; + +import * as actionTypes from '../constants/helloWorldConstants'; + +export const $$initialState = Immutable.fromJS({ + name: '', // this is the default state that would be used if one were not passed into the store +}); + +export default function helloWorldReducer($$state = $$initialState, action) { + const { type, name } = action; + + switch (type) { + case actionTypes.HELLO_WORLD_NAME_UPDATE: { + return $$state.set('name', name); + } + + default: { + return $$state; + } + } +} diff --git a/client/app/bundles/HelloWorld/reducers/index.jsx b/client/app/bundles/HelloWorld/reducers/index.jsx new file mode 100644 index 0000000..1556aef --- /dev/null +++ b/client/app/bundles/HelloWorld/reducers/index.jsx @@ -0,0 +1,14 @@ +// This file is our manifest of all reducers for the app. +// See also /client/app/bundles/HelloWorld/store/helloWorldStore.jsx +// A real world app will likely have many reducers and it helps to organize them in one file. +// `https://github.com/shakacode/react_on_rails/tree/master/docs/additional_reading/generated_client_code.md` +import helloWorldReducer from './helloWorldReducer'; +import { $$initialState as $$helloWorldState } from './helloWorldReducer'; + +export default { + $$helloWorldStore: helloWorldReducer, +}; + +export const initalStates = { + $$helloWorldState, +}; diff --git a/client/app/bundles/HelloWorld/startup/HelloWorldApp.jsx b/client/app/bundles/HelloWorld/startup/HelloWorldApp.jsx index 46e76ec..e5e9f92 100644 --- a/client/app/bundles/HelloWorld/startup/HelloWorldApp.jsx +++ b/client/app/bundles/HelloWorld/startup/HelloWorldApp.jsx @@ -1,9 +1,18 @@ import React from 'react'; +import { Provider } from 'react-redux'; + +import createStore from '../store/helloWorldStore'; import HelloWorld from '../containers/HelloWorld'; +// See documentation for https://github.com/rackt/react-redux. +// This is how you get props from the Rails view into the redux store. +// This code here binds your smart component to the redux store. const HelloWorldApp = props => { + const store = createStore(props); const reactComponent = ( - + + + ); return reactComponent; }; diff --git a/client/app/bundles/HelloWorld/store/helloWorldStore.jsx b/client/app/bundles/HelloWorld/store/helloWorldStore.jsx new file mode 100644 index 0000000..629969b --- /dev/null +++ b/client/app/bundles/HelloWorld/store/helloWorldStore.jsx @@ -0,0 +1,35 @@ +import { compose, createStore, applyMiddleware, combineReducers } from 'redux'; + +// See https://github.com/gaearon/redux-thunk and http://redux.js.org/docs/advanced/AsyncActions.html +// This is not actually used for this simple example, but you'd probably want to use this once your app has +// asynchronous actions. +import thunkMiddleware from 'redux-thunk'; + +// This provides an example of logging redux actions to the console. +// You'd want to disable this for production. +import loggerMiddleware from 'lib/middlewares/loggerMiddleware'; + +import reducers from '../reducers'; +import { initalStates } from '../reducers'; + +export default props => { + // This is how we get initial props Rails into redux. + const { name } = props; + const { $$helloWorldState } = initalStates; + + // Redux expects to initialize the store using an Object, not an Immutable.Map + const initialState = { + $$helloWorldStore: $$helloWorldState.merge({ + name: name, + }), + }; + + const reducer = combineReducers(reducers); + const composedStore = compose( + applyMiddleware(thunkMiddleware, loggerMiddleware) + ); + const storeCreator = composedStore(createStore); + const store = storeCreator(reducer, initialState); + + return store; +}; diff --git a/client/app/lib/middlewares/loggerMiddleware.js b/client/app/lib/middlewares/loggerMiddleware.js new file mode 100644 index 0000000..1253fe1 --- /dev/null +++ b/client/app/lib/middlewares/loggerMiddleware.js @@ -0,0 +1,20 @@ +/* eslint no-console: 0 */ + +// This logger should be configured not to run in a production environment. +// See https://github.com/petehunt/webpack-howto#6-feature-flags for you might turn this off for production. +export default function logger({ getState }) { + return next => action => { + console.log('will dispatch', action); + + // Call the next dispatch method in the middleware chain. + const result = next(action); + + const immutableState = getState(); + + console.log('state after dispatch', JSON.stringify(immutableState)); + + // This will likely be the action itself, unless + // a middleware further in chain changed it. + return result; + }; +} diff --git a/client/package.json b/client/package.json index 00373ab..38f1eff 100644 --- a/client/package.json +++ b/client/package.json @@ -41,13 +41,20 @@ "es5-shim": "^4.3.1", "es6-promise": "^3.0.2", "expose-loader": "^0.7.1", + "immutable": "^3.7.5", "imports-loader": "^0.6.5", "jquery": "^2.1.4", "jquery-ujs": "^1.1.0-1", "loader-utils": "^0.2.11", + "lodash": "^3.10.1", + "mirror-creator": "0.0.1", "react": "^0.14.0", "react-bootstrap": "^0.28.1", "react-dom": "^0.14.3", + "react-redux": "^4.0.0", + "redux": "^3.0.4", + "redux-promise": "^0.5.0", + "redux-thunk": "^1.0.0", "webpack": "^1.12.8" }, "devDependencies": { diff --git a/config/secrets.yml b/config/secrets.yml index b234fc0..76ffeab 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -11,10 +11,10 @@ # if you're sharing your code publicly. development: - secret_key_base: 61253371a60c2e895b0f439a372de7175f88accf858b43c1f4638ae5fe1991d594fa2c7d6c5a13e385a96399f0386291b34a2f973d34f687f44480c07dc820a5 + secret_key_base: 5393f8f843ef2a16a42aa4c037f1e4dc7ad36b2e7d7d3fe17439e27d05798d8e91d11d7c75d9968dad2b105ed50d0c69bb3cee166a731f2efcbb0c9ee38f3514 test: - secret_key_base: 2fc9c95f86cd26ae15f34870dbfe4f4c65003347ef70c2e14074bf99f5c58f692a1aa6ad0ab8063eb2c33f09caad3cb1c06292ef29fe1e429100242d07c641d8 + secret_key_base: 9bb808acd13cbef945156389ae83d3741c55dbc71ff1d53eb556df8f43c0084300eaf1abcc612410f774f0b142fa280c04ee109942c4636d55fe1a35fba36fec # Do not keep production secrets in the repository, # instead read values from the environment.