Last active
February 15, 2017 00:06
-
-
Save nonlogos/80dc760c4182d90c248fb7e942544577 to your computer and use it in GitHub Desktop.
REACT NOTES
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { Component } from 'react'; | |
| export default class App extends Component { | |
| render() { | |
| return ( | |
| <div> | |
| React starter | |
| {this.props.children} //for all the nested child components to show up when nested route is called | |
| </div> | |
| ); | |
| }; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| import { Component } from 'react'; | |
| import BookList from '../containers/book-list'; | |
| export default class App extends Component { | |
| render() { | |
| return ( | |
| <div> | |
| <BookList /> | |
| </div> | |
| ); | |
| } | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 1. Build app with dummy data | |
| 2. replace dummy data with ajax | |
| 3. write middleware to help fetch data |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // -------------------------------------- | |
| // src/index.js | |
| import React from 'react'; | |
| import ReactDOM from 'react-dom'; | |
| import { Provider } from 'react-redux'; | |
| import { createStore, applyMiddleware } from 'redux'; | |
| import { Router, Route, IndexRoute, browserHistory } from 'react-router'; | |
| import reduxThunk from 'redux-thunk'; | |
| import App from './components/app'; | |
| import Signin from './components/auth/signin' | |
| import reducers from './reducers'; | |
| const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); | |
| ReactDOM.render( | |
| <Provider store={createStoreWithMiddelware(redcuers)}> | |
| <Router history={browserHistory}> | |
| <Route path="/" component={App}> | |
| <Route path="signin" component={Signin} /> | |
| </Route> | |
| </Router> | |
| </Provider> | |
| , document.querySelector('.container')); | |
| // -------------------------------------- | |
| // src/components/app.js | |
| import React, { Components } from 'react'; | |
| import Header from './header'; | |
| export default class App extends Component { | |
| render() { | |
| return ( | |
| <div> | |
| <Header /> | |
| {this.props.children} | |
| </div> | |
| ) | |
| } | |
| } | |
| // -------------------------------------- | |
| // src/components/header.js | |
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| import { Linke } from 'react-router'; | |
| class Header extends Component { | |
| renderLinks() { | |
| if (this.prop.authenticated) { | |
| // show a link to sign out | |
| return | |
| <li className="nav-item"> | |
| <Link to="/signout" className="nav-link">Sign out</Link> | |
| </li> | |
| } else { | |
| // show a link to sign in or sign up | |
| // render two elements next to each other but without a div wrapper | |
| return [ | |
| <li className="nav-item" key={1}> | |
| <Link to="/signin" className="nav-link">Sign in</Link> | |
| </li>, | |
| <li className="nav-item" key={2}> | |
| <Link to="/signup" className="nav-link">Sign up</Link> | |
| </li> | |
| ]; | |
| } | |
| } | |
| render() { | |
| return ( | |
| <nav classname="navbar navbar-light"> | |
| <Link to="/">Home page</Link> | |
| <ul className="nav navbar-nav"> | |
| </ul> | |
| </nav> | |
| ) | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { | |
| authenticated: state.auth.authenticated | |
| } | |
| } | |
| export default connect(mapStateToProps)(Header); | |
| // -------------------------------------- | |
| // src/components/auth/signin.js | |
| // install redux form: redux-form | |
| import React, { Component } from 'react'; | |
| import { reduxForm } from 'redux-form'; | |
| import * as actions from '../actions' | |
| class Signin extends Component { | |
| handleFormSubmit({email, password}) { | |
| console.log(email, password); | |
| // need to do something to log user in | |
| this.props.signinUser({email, password}); | |
| } | |
| renderAlert() { | |
| if (this.props.errorMessage) { | |
| return ( | |
| <div className="alert-danger"> | |
| <strong>Oops</strong> {this.props.errorMessage} | |
| </div> | |
| ); | |
| } | |
| } | |
| render() { | |
| const { handleSubmit, fields: {email, password} } = this.props; | |
| return ( | |
| <form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}> | |
| <fieldset className="form-group"> | |
| <label>Email:</label> | |
| <input {...email} type="email" className="form-control" /> | |
| </fieldset> | |
| <fieldset className="form-group"> | |
| <label>Password:</label> | |
| <input {...password} type="password" className="form-control" /> | |
| </fieldset> | |
| {this.renderAlert()}; | |
| <button action="submit" calssName="btn btn-primary">Sign in</button> | |
| </form> | |
| ) | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { errorMessage: state.auth.error }; | |
| } | |
| export default reduxForm({ | |
| form: 'signin', // name of the form | |
| fields: ['email', 'password'] | |
| }, mapStateToProps, actions)(Signin); // redux form is the same as connect. after the first argument object, the second is mapStateToProps and 3rd is actions | |
| // -------------------------------------- | |
| // src/reducers/index.js | |
| import { combineReducers } from 'redux'; | |
| impoort { reducer as form } from 'redux-form'; // ES6 to rename import using "as"; | |
| import authReducer from './auth_reducer'; | |
| const rootReducer = combineReducers({ | |
| form, // equivalent to form: form | |
| auth: authReducer | |
| }); | |
| export default rootReducer; | |
| // -------------------------------------- | |
| // src/reducers/auth_reducer.js | |
| import { AUTH_USER, UNAUTH_USER, AUTH_ERROR } from '../actions/types' | |
| export default function(state = {}, action) { | |
| switch(action.type) { | |
| case AUTH_USER: | |
| return { ...state, authenticated: true }; | |
| case UNAUTH_USER: | |
| return { ...state, authenticated: false }; | |
| case AUTH_ERROR: | |
| return { ...state, error: action.payload }; | |
| } | |
| return state; | |
| } | |
| // -------------------------------------- | |
| // src/actions/types.js | |
| export const AUTH_USER = 'auth_user'; | |
| export const UNAUTH_USER = 'unauth_user'; | |
| export const AUTH_ERROR = 'auth_error'; | |
| // -------------------------------------- | |
| // src/actions/index.js | |
| // install redux-thunk to give us direct access to action dispatch and can return a function instead of an object: allows us to dispatch our own actions at any time we want | |
| // install axios | |
| // make sure the api server and mongod are running (if using mongodb) | |
| import axios from 'axios'; | |
| import { browserHistory } from 'react-router' // communicate information about the url from react router, can also be used to make changes as well | |
| import { AUTH_USER } from './types'; | |
| import { AUTH_ERROR } from './types'; | |
| const ROOT_URL = 'http://localhost:3090'; | |
| export function signinUser({email, password}) { | |
| // reduxThunk middleware enables us to return a function with the dispatch method | |
| return function(dispatch) { | |
| // submit email/password to server | |
| axios.post(`${ROOT_URL}/signin`, { email, password }) // es6 shortcut syntax because key and value are the same | |
| .then(response => { | |
| // if request is good => | |
| // update state to indicate user is authenticated | |
| dispatch({type: AUTH_USER}); | |
| // save the jwt token using localStorage (harddrive data on the user's machine /not shared across user devices) and not shared across domains | |
| // - 1. so when user makes a request that needs to be authenticated, attached the JWT | |
| // - 2. When user revisits our app, JWT is still available, no need to login again | |
| localStorage.setItem('token', response.data.token); // localStorage is an object available on window scope. so no need to import | |
| // can check via browser console with: localStorage.getItem('token'); | |
| // redirect to the route '/feature' using react router | |
| browserHistory.push('/feature'); | |
| }) | |
| .catch(() => { | |
| // if request is bad => | |
| // show an error to the user | |
| dispatch(authError('bad login info')); | |
| }) | |
| } | |
| } | |
| export function authError(error) { | |
| return { | |
| type: AUTH_ERROR, | |
| payload: error | |
| } | |
| } | |
| // -------------------------------------- | |
| // src/components/feature.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Containers - smart components: subscribe to reducer app states | |
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| //for actions setup | |
| import { selectBook } from '../actions/index'; | |
| import {bindActionCreators} from 'redux'; | |
| class Booklist extends Component { | |
| // to set up component state, we have to initialize react class constructor | |
| constructor(props) { | |
| super(props); | |
| this.state = { term: '' }; //add a property 'term' to the component state and default to empty string | |
| } | |
| renderList() { | |
| return this.props.books.map(book => { | |
| return ( | |
| <li key={ book.title }>{ book.title }</li> | |
| ); | |
| }); | |
| } | |
| // if there are alot of props to pass down, use the es2017 spread operator ... | |
| return ( | |
| <showCard key={show.imdbId} {...show} /> // equals: <showCard key={show.imdbId} poster={show.poster} title={show.title}... | |
| ) | |
| render() { | |
| return ( | |
| <ul className="list-group col-sm-4"> | |
| { this.renderList() } | |
| </ul> | |
| ) | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| // whatever is returned will show up as props inside of booklist | |
| return { | |
| books: state.books | |
| } | |
| } | |
| //Anything returned from this function will end up as props on the BookList container | |
| function mapDispatchToProps(dispatch) { | |
| // whenever selectBook is called result should be passed to all of our reducers | |
| return bindActionCreators({selectBook: selectBook}, dispatch); | |
| } | |
| // Promote BookList from a component to a container - it needs to know about this new dispatch method, selectBook | |
| // and make it available as a prop | |
| export default connect(mapStateToProps, mapDispatchToProps)(BookList); | |
| // | |
| // example 2 bind input to action | |
| // | |
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| import { bindActionCreators } from 'redux'; | |
| import { fetchWeather } from '../actions/index'; | |
| class SearchBar extends Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { term: '' }; | |
| //force bind an instance of this.onInputChange to the component SearchBar to avoid losing the this reference when function is called in JSX | |
| this.onInputChange = this.onInputChange.bind(this); | |
| this.onFormSubmit = this.onFormSubmit.bind(this); | |
| } | |
| onInputChange(event) { | |
| this.setState({ term: event.target.value }); | |
| } | |
| onFormSubmit(event) { | |
| event.preventDefault(); | |
| this.props.fetchWeather(this.state.term); | |
| this.setState({term: ''}); | |
| } | |
| render() { | |
| return( | |
| <form onSubmit={ this.onFormSubmit } className="input-group"> | |
| <input | |
| placeholder="Get a five-day forecast in your favorite cities" | |
| className="form=control" | |
| value={ this.state.term } | |
| onChang={ this.onInputChange } | |
| /> | |
| <span className="input-group-btn"> | |
| <button type="submit" className="btn btn-secondary">Submit</button> | |
| </span> | |
| </form> | |
| ) | |
| } | |
| } | |
| function mapDispatchToProps(dispatch) { | |
| return bindActionCreators({ fetchWeather }, dispatch); | |
| } | |
| // connect that only matching dispatch and not state props | |
| export default connect(null, mapDispatchToProps(SearchBar)); | |
| // --------------------------------------- | |
| // mapDispatchToProps shortcut | |
| import React, {Component} from 'react'; | |
| import { connect } from 'react-redux'; | |
| import * as actions from '../actions'; // import all the actions into variable "actions" | |
| class CommentBox extends Component { | |
| ... | |
| } | |
| // pass in all the actions into connect instead of using matchDispatchToProp | |
| export default connect(null, actions)(CommentBox) //first argument is reserved for mapStateToProps, so if we don't need state, use null |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| import Chart from '../components.chart'; | |
| import GoogleMap from '../components.google_map'; | |
| class WeatherList extends Component { | |
| renderWeather(cityData) { | |
| const name = cityData.city.name; | |
| const temps = cityData.list.map(weather => weather.main.temp); | |
| const pressures= cityData.list.map(weather => weather.main.pressure); | |
| const humidities= cityData.list.map(weather => weather.main.humidity); | |
| const { lon, lat } = cityData.city.coord; | |
| // above line is equivalent to the following two lines. it's destructuring in ES6 | |
| // const lon = cityData.city.coord.lon; | |
| // const lat = cityData.city.coord.lat; | |
| return ( | |
| <tr key={name}> // key needs to be in parent container | |
| <td><GoogleMap lon={lon} lat={lat} /></td> | |
| <td><Chart data={temps} color="orange" units="K"/></td> | |
| <td><Chart data={pressures} color="green" units="hPa" /></td> | |
| <td><Chart data={humidities} color="black" units="%" /></td> | |
| </tr> | |
| ) | |
| } | |
| render() { | |
| return ( | |
| <table className="table table-hover"> | |
| <thead> | |
| <tr> | |
| <th>City</th> | |
| <th>Temperature (K)</th> | |
| <th>Pressure (hPa)</th> | |
| <th>Humidity (%)</th> | |
| </tr> | |
| </thead> | |
| <tBody> | |
| {this.props.weather.map(this.renderWeather)} | |
| </tBody> | |
| </table> | |
| ) | |
| } | |
| } | |
| function mapStateToProps({ weather }) { // {weather} === const weather = state.weather | |
| return { weather } // { weather } === { weather: weather } | |
| } | |
| export default connect(mapStateToProps)(WeatherList); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| https://react.rocks/example/react-dates | |
| http://airbnb.io/react-dates/?selectedKind=DateRangePicker&selectedStory=3%20months&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel | |
| https://github.com/chrisharrington/a-react-datepicker/blob/master/a-react-datepicker.js | |
| http://chrisharrington.github.io/demos/react/date-picker/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Dynamic route component: PostsShow | |
| // | |
| import React, { component, PropTypes } from 'react'; | |
| import { connect } from 'react-redux'; | |
| import { fetchPost, deletePost } from '../actions/index'; | |
| import { Link } from 'react-router'; | |
| class PostsShow extends Component { | |
| static contextType = { | |
| router: PropTypes.object | |
| }; | |
| componentWillMount() { | |
| this.props.fetchPost(this.props.params.id); //getting the post id from the params object passed in by the dynamic router (this.props.params) | |
| } | |
| onDeleteClick() { | |
| this.props.deletePost(this.props.params.id) | |
| .then(() => this.context.router.push('/')) | |
| } | |
| render() { | |
| const { post } = this.props; | |
| if (!post) { | |
| return <div>Loading...</div> | |
| } | |
| return { | |
| <div> | |
| show post {this.props.params.id} | |
| <Link to="/">Back to Index</Link> | |
| <button | |
| className="btn btn-danger pull-xs-right" | |
| onClick={this.onDeleteClick.bind(this)}> | |
| Delete Post | |
| </button> | |
| <h3>{post.title}</h3> | |
| <h6>Categories: {post.categories}</h6> | |
| <p>{post.content}</p> | |
| </div> | |
| ) | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { post: state.posts.post } | |
| } | |
| export default connect(mapStateToProps, { fetchPost, deletePost })(PostsShow); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import _ from 'lodash'; | |
| import React from 'react'; | |
| import { Sparklines, SparklinesLine } from 'react-sparklines; | |
| function average(data) { | |
| return _.round(_.sum(data)/data.length); //using lodash to sum the number and get the average. Round number in case of decimal result | |
| } | |
| export default (props) => { | |
| return ( | |
| <div> | |
| <Sparklines height={120} width={180} data={props.data}> | |
| <SparlinesLine color={props.color} /> | |
| </Sparklines> | |
| <div>{average(props.data)} {props.units}</div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| import { GoogleMapLoader, GoogleMap } from 'react-google-maps' | |
| export default (props) => { | |
| return ( | |
| <GoogleMapLoader | |
| containerElement={ <div style={{height: '100%'}} /> } | |
| googleMapElement={ | |
| <GoogleMap defaultZoom={12} defaultCenter={{lat: props.lat, lng: props.lon}} /> | |
| } | |
| /> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // regular component + additional functionality or data = enhanced or composed component | |
| // shared logics that's not specific and can be used across diff components | |
| // example: A resourceList component + a require_auth HOC = composed component that will check authentication sttus before rendering | |
| // --------------------------------------------------------- | |
| // 1. set up the overall scaffolding | |
| // --------------------------------------------------------- | |
| // ------------------------------ | |
| // index.js | |
| import React from 'react'; | |
| import ReactDOM from 'react-dom'; | |
| import { Provider } from 'react-redux'; | |
| import { createStore, applyMiddleware } from 'redux'; | |
| import { Router, Route, browserHistory } from 'react-router'; | |
| //import the HOC | |
| import requireAuth from './components/require_authentication'; | |
| import App from './components/app'; | |
| import Resources from './components/resources' | |
| import reducers from './reducers'; | |
| const createStoreWithMiddleware = applyMiddleware()(createStore); | |
| ReactDOM.render( | |
| <Provider store={createStoreWithMiddleware(reducers)}> | |
| <Router history={browserHistory}> | |
| <Route path="/" component={App}> | |
| <Route path="/resources" component={requireAuth(Resources)} /> //wrap resources with requireAuth HOC | |
| </Route> | |
| </Router> | |
| </Provider> | |
| , document.querySelector('.container')); | |
| // ------------------------------ | |
| // app.js | |
| import React, { Component } from 'react'; | |
| import Header from './header' | |
| export default class App extends Component { | |
| render() { | |
| return( | |
| <div> | |
| <Header /> | |
| {this.props.children} //part of react router, if our app root route has any chidren, show them when component is active | |
| </div> | |
| ) | |
| } | |
| } | |
| // ------------------------------ | |
| // header.js | |
| import React, { Component } from 'react'; | |
| import { Link } from 'react-router'; | |
| import { connect } from 'react-redux'; | |
| import * as actions from '../actions'; | |
| class Header extends Component { | |
| authButton() { | |
| if (this.props.authenticated) { | |
| return <button onClick={() => this.props.authenticate(false)}>Sign Out</button> | |
| } | |
| return <button onClick={() => this.props.authenticate(true)}>Sign In</button> | |
| } | |
| render() { | |
| return( | |
| <nav className="navbar navbar-light"> | |
| <ul> | |
| <li> | |
| <Link to="/">Home</Link> | |
| </li> | |
| <li> | |
| <Link to="/resources">Resources</Link> | |
| </li> | |
| <li> | |
| {this.authButton()} | |
| </li> | |
| </ul> | |
| </nav> | |
| ); | |
| }; | |
| }; | |
| function mapStateToProps(state) { | |
| return { authenticated: state.authenticated }; | |
| } | |
| export default connect(mapStateToProps, actions)(Header); | |
| // ------------------------------ | |
| // resources.js | |
| import React from 'react'; | |
| export default () => { | |
| return ( | |
| <div> | |
| Super Secret Recipe | |
| <ul> | |
| <li>Hello</li> | |
| </ul> | |
| </div> | |
| ) | |
| } | |
| // ------------------------------ | |
| // src/actions/types.js | |
| export const CHANGE_AUTH = 'change_auth'; | |
| // ------------------------------ | |
| // src/actions/index.js | |
| import { | |
| CHANGE_AUTH | |
| } from './types'; | |
| export function authenticate(isLoggedIn) { | |
| return { | |
| type: CHANGE_AUTH, | |
| paylad: isLoggedIn | |
| } | |
| } | |
| // ------------------------------ | |
| // src/reducer/index.js | |
| import { combineReducers } from 'redux'; | |
| import authenticationReducer from './authentication'; | |
| const rootReducer = combineReducers({ | |
| authenticated: authenticationReducer | |
| }); | |
| export default rootReducer; | |
| // ------------------------------ | |
| // src/reducer/authentication.js | |
| import {CHANGE_AUTH} from '../actions/types'; | |
| export default function(state = false, action) { | |
| switch (action.type) { | |
| case CHANGE_AUTH: | |
| return action.payload; | |
| } | |
| return state; | |
| } | |
| // --------------------------------------------------------- | |
| // 2. Higer Order Components | |
| // --------------------------------------------------------- | |
| // higher order components (should be put inside a nested component directory) | |
| // src/components/require_authentication.js | |
| // ------------------------------ | |
| // basic HOC template | |
| import React, { Component } from 'react'; | |
| export default function(ComposedComponent) { | |
| class Authentication extends Component { | |
| render() { | |
| console.log(this.props.resources) // => resourceList | |
| return <ComposedCompoent {...this.props} /> //handles any future props that may be passed into the wrapped components as it's used | |
| } | |
| } | |
| return Authentication; | |
| } | |
| // in some other location..not in this file...we want to use this HOC | |
| // example of how to use HOC | |
| // import Authentication //this is my HOC | |
| // import Resources // the component I want to wrap with HOC | |
| // const ComposedComponent = Authentication(Resources); | |
| // in some render method... | |
| // <ComposedComponent resources={resourceList}/> | |
| // ------------------------------ | |
| // Authentication HOC Component example | |
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| export default function(ComposedComponent) { | |
| class Authentication extends Component { | |
| // context is something just like props but allows us to skip the React component hierachy. it's from react router and is used for changing routes | |
| // to use context, we need to first define the contextTypes to avoid abusing context instead of using props | |
| // static: class-level property or method - saying static contextTypes = object => gives other parts of the app access to Authentication.contextTypes | |
| static contextTypes = { | |
| router: React.PropTypes.object | |
| } | |
| componentWillMount() { | |
| if (!this.props.authenticated) { | |
| this.context.router.push('/') // kicks it back to the root directory automatically without links | |
| } | |
| } | |
| // whenever a component gets a new set of props. when user logs out, they need to get kicked back to home route as well. | |
| componentWillUpdate(nextProps) { | |
| if (!nextProps.authenticated) { | |
| this.context.router.push('/') // kicks it back to the root directory automatically without links | |
| } | |
| } | |
| render() { | |
| return <ComposedCompoent {...this.props} /> | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { authenticated: state.authenticated }; | |
| } | |
| return connect(mapStateToProps)(Authentication); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| import ReactDom from 'react-dom'; | |
| import { Provider } from 'react-redux'; | |
| import { createStore, applyMiddleware } from 'redux'; | |
| import ReduxPromise from 'redux-promise'; | |
| import App from './components/app'; | |
| import reducers from './reducers'; | |
| const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore); | |
| ReactDom.render( | |
| <Provider store={createStoreWithMiddleware(reducers)}> | |
| <App /> | |
| </Provider> | |
| , document.querySelector('.container')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // React component calls on action creator | |
| // Action creator returns an action that's sent to middle ware | |
| // middleware has the opportunity to log, stop, modify, or not touch an action | |
| // then middleware forwards the action to reducers | |
| // reducers produces new states based on the action | |
| // and the new states are sent back to react | |
| // --------------------------------------------- | |
| // src/actions/types.js | |
| export const FETCH_USERS = 'fetch_users'; | |
| // --------------------------------------------- | |
| // src/reducers/users.js - static data | |
| import { FETCH_USERS } from '../actions/types'; | |
| export default function(state = [], action) { | |
| switch (action.type) { | |
| case FETCH_USERS: | |
| return [ ...state, ...action.payload ]; | |
| } | |
| return state; | |
| } | |
| // --------------------------------------------- | |
| // src/reducers/users.js - ajax request | |
| import { FETCH_USERS } from '../actions/types'; | |
| export default function(state = [], action) { | |
| switch (action.type) { | |
| case FETCH_USERS: | |
| return [ ...state, ...action.payload.data ]; | |
| } | |
| return state; | |
| } | |
| // --------------------------------------------- | |
| // src/reducers/index.js | |
| import { combineReducers } from 'redux'; | |
| import usersReducer from './users'; | |
| const rootReducer = combineReducers({ | |
| users: usersReducer | |
| }) | |
| export default rootReducer; | |
| // --------------------------------------------- | |
| // src/actions/index.js - static data first | |
| import { FETCH_USERS } from './types'; | |
| export function fetchUsers() { | |
| return { | |
| type: FETCH_USERS, | |
| payload: [ | |
| { name: 'James' }, | |
| { name: 'Alex' }, | |
| { name: 'JIm' } | |
| ] | |
| } | |
| } | |
| // --------------------------------------------- | |
| // src/actions/index.js - ajax request | |
| import { FETCH_USERS } from './types'; | |
| import axios from 'axios'; | |
| export function fetchUsers() { | |
| const request = axios.get('http....') | |
| return { | |
| type: FETCH_USERS, | |
| payload: request | |
| } | |
| } | |
| // --------------------------------------------- | |
| // src/components/app.js | |
| import React from 'react'; | |
| import { Component } from 'react'; | |
| import UserList from './user_list'; | |
| export default class App extends Component { | |
| render() { | |
| return ( | |
| <div> | |
| <UserList /> | |
| </div> | |
| ); | |
| } | |
| } | |
| // --------------------------------------------- | |
| // src/components/user_list.js - fake static data | |
| import React, { Component } from 'react';' | |
| import { connect } from 'react-redux'; | |
| import * as actions from '../actions'; | |
| class UserList extends Component { | |
| componentWillMount() { | |
| this.props.fetchUsers(); | |
| } | |
| renderUser(user) { | |
| return ( | |
| <div className="card card-block"> | |
| <h4 className="card-title">{user.name}</h4> | |
| <p className="card-text">{user.company.name}</p> | |
| <a className="btn btn-primary" href={user.website}>website</a> | |
| </div> | |
| ) | |
| } | |
| render() { | |
| return ( | |
| <div> | |
| {this.props.users.map(this.renderUser)} | |
| </div> | |
| ) | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { users: state.users }; | |
| } | |
| export default connect(mapStateToProps, actions)(UserList); | |
| // --------------------------------------------- | |
| // Middleware!!!! | |
| // src/middlewares/async.js | |
| // default template | |
| // it's a function with dispatch passed in that returns a function with "next" passed in that returns another function with action passed in. | |
| export default function({dispatch}) { | |
| return next => action => { | |
| console.log(action); | |
| return next(action); //send this action on to the next middleware in our stack, if no more send to reducer | |
| }; | |
| }; | |
| // asyn example | |
| export default function({dispatch}) { | |
| return next => action => { | |
| // does action contains a promise, if so wait for it to resolve | |
| // create a new action and send it thru all the middleware again but with the data as payload not the promise | |
| //handle no promise to pass to the next middleware | |
| if (!action.payload || !action.payload.then) { | |
| return next(action); | |
| } | |
| // make sure the action's promise resolves | |
| action.payload | |
| .then( response => dispatch({ ...action, payload:response }) ) // create a new action with all the data contained in the action but replace the old promise payload to the new response and dispatch the new action from the top of the middleware again with the data | |
| }; | |
| }; | |
| // --------------------------------------------- | |
| // src/index.js | |
| import React from 'react'; | |
| import ReactDOM from 'react-dom'; | |
| imoprt { Provider } from 'react-redux'; | |
| import { createStore, applyMiddleware } from 'redux'; | |
| import App from './components/app'; | |
| import reducers from './reducers'; | |
| import Async from './middlewares/async' | |
| const createStoreWithMiddleware = applyMiddleware(Async, logger)(createStore); //add Async middleware to the redux store and other middlewares separated by comma | |
| ReactDOM.render( | |
| <Provider store={createStoreWithMiddleware(reducers)}> | |
| <App /> | |
| </Provider> | |
| , document.querySelector('.container')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Source: https://facebook.github.io/react/docs/typechecking-with-proptypes.html | |
| class Greeting extends React.Component { | |
| render() { | |
| return ( | |
| <h1>Hello, {this.props.name}</h1> | |
| ); | |
| } | |
| } | |
| Greeting.propTypes = { | |
| name: React.PropTypes.string | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // to specify the prop types for the props that are being passed down from parent components | |
| // or use Flo or typescript | |
| const {shape, string} = React.PropTypes | |
| const Search = React.createClass({ | |
| propTypes: { | |
| show: shape({ | |
| poster: string, | |
| title: string, | |
| year: string, | |
| description: string.isRequired | |
| }) | |
| } | |
| render() { | |
| const { poster, title, year, description } = this.props.show; | |
| ... | |
| } | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Actions: index.js | |
| // | |
| import axios from 'axios'; | |
| const API_KEY = '...'; | |
| const ROOT_URL = `http://api.openweathermap.org/data/2.5/forecast?appid=${API_KEY}`; | |
| export const FETCH_WEATHTER = 'FETCH_WEATHTER'; | |
| export const FETCH_POST = 'FETCH_POST'; | |
| export const DELETE_POST = 'DELETE_POST'; | |
| export function fetchWeather(city){ | |
| const url = `${ROOT_URL}&q=${city}, us`; | |
| const request = axios.get(url); | |
| return { | |
| type: FETCH_WEATHTER, | |
| payload: request | |
| } | |
| } | |
| export function fetchPost(id) { | |
| const request = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`); | |
| return { | |
| type: FETCH_POST, | |
| payload: request | |
| } | |
| } | |
| export function deletePost(id) { | |
| const request = axios.delete(`${ROOT_URL}/posts/${id}${API_KEY}`) | |
| return { | |
| type: DELETE_POST, | |
| payload: request | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <BrowserRouter> | |
| <div> | |
| <nav> | |
| <ul> | |
| { this.props.data.markets ? | |
| this.props.data.markets.map(market => { | |
| return <li key={market.id}><Link to={`/markets/${market.name}`}>{market.name}</Link></li> | |
| }) | |
| : <div>Loading</div> | |
| } | |
| </ul> | |
| </nav> | |
| <section> | |
| <Match exactly pattern="/markets/:market" render={ | |
| (defaultProps) => <MarketContainer data={this.props} {...defaultProps} /> | |
| } /> | |
| </section> | |
| </div> | |
| </BrowserRouter> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //component | |
| import React, { Component } from 'react'; | |
| import { connect } from 'react-redux'; | |
| import { fetchPosts } from '../actions/index'; | |
| import { Link } from 'react-router'; //add links to a component | |
| class PostsIndex extends Component { | |
| componentWillMount() { | |
| //a life cycle method that react will call whenever a component is rendered for the first time in the dom. Will not be called on subsequent rerenders. | |
| this.props.fetchPosts(); | |
| } | |
| renderPost() { | |
| return this.props.posts.map(post => { | |
| return ( | |
| <li className="list-group-item" key={post.id}> | |
| <Link to={"posts/" + post.id}> | |
| <span className="pull-xs-right">{post.categories}</span> | |
| <strong>{post.title}</strong> | |
| </Link> | |
| </li> | |
| ); | |
| }) | |
| } | |
| render() { | |
| return ( | |
| <div> | |
| <div className="text-xs-right"> | |
| <Link to="/posts/new" className="btn btn-primary"> | |
| Add a Post | |
| </Link> | |
| </div> | |
| <h3>Posts</h3> | |
| <ul className="list-group"> | |
| {this.renderPost()} | |
| </ul> | |
| </div> | |
| ); | |
| } | |
| } | |
| function mapStateToProps(state) { | |
| return { posts: state.posts.all } | |
| } | |
| export default connect(mapStateToProps, { fetchPosts })(PostsIndex); // short cut to bypass the function mapDispatchToProps |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Components to build a React app | |
| -> node_modules | |
| -> src | |
| -> actions | |
| -> index.js | |
| -> components | |
| -> app.js | |
| -> containers | |
| -> reducers | |
| -> index.js | |
| -> index.js | |
| -> routes.js | |
| -> style //folder | |
| -> .gitignore | |
| -> index.html | |
| -> package.json | |
| -> webpack.config.js |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //index.js | |
| import React from 'react'; | |
| import ReactDom from 'react-dom'; | |
| import { Provider } from 'react-redux'; | |
| import { createStore, applyMiddleware } from 'redux'; | |
| import { Router, browserHistory } from 'react-router'; | |
| import reducers from './reducers'; | |
| import route from './routes'; | |
| const createStoreWithMiddleware = applyMiddleware()(createStore); | |
| ReactDom.render( | |
| <Provider store={createStoreWithMiddleware(reducers)}> | |
| <Router history={browserHistory} routes={routes} /> | |
| </provider> | |
| ); | |
| //routes.js (inside src) | |
| import React from 'react'; | |
| import { Route, IndexRoute } form 'react-router'; | |
| import App from './components/app'; | |
| import PostsIndex from './components/posts_index'; | |
| import PostsNew from './components/posts_new'; | |
| import PostsShow from './components/posts_show' | |
| export default ( | |
| <Route path="/" component={App}> | |
| <IndexRoute component={PostsIndex} />//show only if path matches the parent | |
| <Route path="posts/new" component={PostsNew} /> //nested routes | |
| <Route path="posts/:id" component={PostsShow} /> //dynamic route this.props.params.id | |
| </Route> | |
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Example for dynamic reducer | |
| // | |
| import { FETCH_WEATHER } from '../actions/index'; | |
| export default function(state = [], action) { //initialize state to an array cause the data will be an array later | |
| switch (action.type) { | |
| case FETCH_WEATHER: | |
| return [ action.payload.data, ...state ]; // creates a new array - immutable and concat/flatten new data with existing data -ES6 | |
| } | |
| return state; //default | |
| } | |
| // | |
| // Example 2 reducers/reducer_posts.js | |
| // | |
| import { FETCH_POSTS, FETCH_POST } from '../actions/index'; | |
| const INITIAL_STATE = { all: [], post: null }; | |
| export default function(state=INITIAL_STATE, action) { | |
| switch(action.type) { | |
| case FETCH_POST: | |
| return { ...state, post: action.payload.data}; | |
| case FETCH_POSTS: | |
| return { ...state, all: action.payload.data }; //make a new object with existing state and assign action.payload.data to the 'all' property | |
| default: | |
| return state; | |
| } | |
| } | |
| // | |
| // example for custom reducers reducer_books.js | |
| // | |
| //default - basic structure | |
| export default function() { | |
| return [ | |
| {title: 'Javascript: The Good Parts'}, | |
| {title: 'Harry Potter'}, | |
| {title: 'The Dark Tower'}, | |
| {title: 'Eloquent Ruby'} | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Index.js | |
| // | |
| import { combineReducers } from 'redux'; | |
| import BooksReducer from './reducer_books' // custom reducers - direct path | |
| import WeatherReducer from './reducer_weather'; | |
| import { reducer as formReducer } from 'redux-form' // using redux form library | |
| // set all app states and data | |
| const rootReducer = combineReducers({ | |
| books: BooksReducer, | |
| weather: WeatherReducer, | |
| form: formReducer | |
| }) | |
| export default rootReducer; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Redux form reducer > index.js | |
| // | |
| import { combineReducers } from 'redux'; | |
| import { reducer as formReducer } from 'redux-form' // using redux form library | |
| // set all app states and data | |
| const rootReducer = combineReducers({ | |
| form: formReducer | |
| }) | |
| export default rootReducer; | |
| // | |
| // redux form component | |
| // | |
| import React, { Component, PropTypes } from 'react'; | |
| import { reduxForm } from 'redux-form'; | |
| import { Link } from 'react-router'; | |
| import { createPost } from '../actions/index'; | |
| class PostsNew extends Component { | |
| static contextTypes = { | |
| router: PropTypes.object | |
| }; //getting context from the Router component | |
| onSubmit(props) { | |
| this.props.createPost(props) | |
| .then(() => { | |
| // blog post has been created. navigate the user to the index page | |
| // navigate by calling this.context.router.push with the new path to navigate to | |
| this.context.router.push('/'); | |
| }) | |
| } | |
| render() { | |
| const { fields: { title, categories, content }, handleSubmit } = this.props; //assign variable handleSubmit to this.props.handleSubmit from redux form | |
| //const { fields: { title, categories, content } = this.props === const title = this.props.fields.title... | |
| return ( | |
| <form onSubmit={handleSubmit(this.onSumbit.bind(this))}> | |
| <h3>Create A New Post</h3> | |
| <div className={`form-group ${title.touched && title.invalid ? 'has-danger' : ''}`}> | |
| <label>Title</label> | |
| <input type="text" className="form-control" {...title} /> //destructuring the object title. passing the object properties on the title object into the input so we can use the individual property such as onChange... | |
| <div className="text-help"> | |
| {title.touched ? title.error : ''} | |
| </div> | |
| </div> | |
| <div className={`form-group ${categories.touched && categories.invalid ? 'has-danger' : ''}`}> | |
| <label>Categories</label> | |
| <input type="text" className="form-control" {...categories} /> | |
| <div className="text-help"> | |
| {categories.touched ? categories.error : ''} | |
| </div> | |
| </div> | |
| <div className={`form-group ${content.touched && content.invalid ? 'has-danger' : ''}`}> | |
| <label>Content</label> | |
| <textarea className="form-control" {...content} /> | |
| <div className="text-help"> | |
| {content.touched ? content.error : ''} | |
| </div> | |
| </div> | |
| <button type="submit" className="btn btn-primary">Submit</button> | |
| <Link to="/" className="btn btn-danger">Cancel</Link> | |
| </form> | |
| ); | |
| } | |
| } | |
| function validate(values) { | |
| const errors = {}; | |
| if (!values.title) { | |
| errors.title = 'Enter a username'; | |
| } | |
| if (!values.categories) { | |
| errors.categories = 'Enter a category'; | |
| } | |
| if (!values.content) { | |
| errors.content = 'Enter some content'; | |
| } | |
| return errors; | |
| } | |
| //reduxForm is just like connect except it needs form config as the first argument. | |
| // frist is form config, 2nd is mapStateToProps, 3rd is mapDispatchToProps | |
| export default reduxForm({ | |
| form: 'PostsNewForm', | |
| fields: ['title', 'categories', 'content'], | |
| validate | |
| }, null, { createPost })(PostsNew); | |
| // | |
| // redux form Actions index.js | |
| // | |
| export const CREATE_POST= 'CREATE_POST' | |
| export function createPost(props) { | |
| const request = axios.post(`${ROOT_URL}/posts${API_KEY}`, props); | |
| return { | |
| type: CREATE_POST, | |
| payload: request | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // actions > index | |
| // | |
| import axios from 'axios'; | |
| export const FETCH_PROFILES = 'FETCH_PROFILES'; | |
| export function fetchUsers() { | |
| const request = axios.get('http://jsonplaceholder.tyicode.com/users'); | |
| return (dispatch) => { | |
| request.then(({data}) => { | |
| dispatch({ type: 'FETCH_PROFILES', payload: data }) | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ------------------------------------ | |
| // dependencies | |
| - jsdom | |
| - jquery | |
| - chai | |
| - chai-jquery | |
| // ------------------------------------ | |
| // test_helper.js | |
| // set up testing environment to run like a browser in the command line | |
| // build 'renderComponent' helper that should render a given react class | |
| // build helper for simulating events | |
| // set up chai-jquery | |
| import _$ from 'jquery'; | |
| import React from 'react'; | |
| import ReactDom from 'react-dom'; | |
| import TestUtils from 'react-addons-test-utils'; | |
| import jsdom from 'jsdom'; | |
| import chai, { expect } from 'chai'; | |
| import chaiJquery from 'chai-jquery'; | |
| import { Provider } from 'react-redux'; | |
| import { createStore } from 'redux'; | |
| import reducers from '../src/reducers'; | |
| // ------------------------------------ | |
| // set up testing environment to run like a browser in the command line | |
| // initialize the fake DOM element for use inside of the terminal where the tests will be run | |
| global.document = jsdom.sjdom('<!doctype html><html><body></body></html>'); //instead of using window as in the browser, this is going to be run in Node, which will be global (global.document) | |
| global.window = global.document.defaultView; | |
| // create our own junky version of jquery $ instance - because we can't use $ to call out to the browser. Instead we want jquery to only worry about the fake DOM element in the testing environment | |
| const $ = _$(global.window); | |
| // ------------------------------------ | |
| // set up chai-jquery | |
| chaiJquery(chai, chai.util, $); | |
| // ------------------------------------ | |
| // build 'renderComponent' helper that should render a given react class | |
| function renderComponent(ComponentClass, props = {}, state = {}) { // props are component-level props(object) and state is any application state that's passed in | |
| const componentInstance = TestUtils.renderIntoDocument( | |
| <Provider store={createStore(reducers, state)}> | |
| <ComponentClass {...props} /> //use the spread operator to get the props to show as top-level properties | |
| </Provider> | |
| ); | |
| return $(ReactDom.findDOMNode(componentInstance)); //produces HTML and wrap in jquery so we can use chaiJquery | |
| } | |
| // ------------------------------------ | |
| // build helper for simulating events by adding a method to jquery so every jquery instance can use the method | |
| $.fn.simulate = function(eventName, value) { | |
| if (value) { | |
| this.val(value); | |
| } | |
| TestUtils.Simulate[eventName](this[0]); | |
| }; | |
| export { renderComponent, expect }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // --------------------------------------------- | |
| //MOCHA && CHAI | |
| // --------------------------------------------- | |
| source: | |
| https://facebook.github.io/react/docs/test-utils.html | |
| https://github.com/chaijs/chai-jquery | |
| http://chaijs.com/api/bdd/ | |
| command: npm run test:watch | |
| // --------------------------------------------- | |
| //app_test.js | |
| // basic breakdown | |
| import { renderComponent, expect } from '../test_helper'; | |
| import App from '../../src/components/app' | |
| // use describe to group together similar tests | |
| describe('App', () => { | |
| // use 'it' to test a single attribute of a target | |
| it('shows the correct class', () => { | |
| // creat an instance of App | |
| const component = renderComponent(App); | |
| //use 'expect' to make an 'assertion' about a target | |
| expect(component).to.have.class('comment-box') | |
| }); | |
| }) | |
| // --------------------------------------------- | |
| // test driven strategies | |
| 1. determine what criterion we need to test for | |
| ex1. comment box: we care about | |
| - it has a text area | |
| - it has a button to submit the comment | |
| - entering text into the text area updates the text | |
| ext2. list of submitted comments: we care about | |
| - it shows a comment in a LI | |
| - given a list of comments, it shows all the comments | |
| // ------------------------------------------------------ | |
| //further examples | |
| // ------------------------------------------ | |
| // in src/components/comment_box | |
| import React, {Component} from 'react'; | |
| import { connect } from 'react-redux'; | |
| import * as actions from '../actions'; | |
| class CommentBox extends Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { | |
| comment: '' | |
| }; | |
| } | |
| handleChange(event) { | |
| this.setState({comment: event.target.value}); | |
| } | |
| handleSubmit(event) { | |
| event.preventDefault(); | |
| //call the specific action and pass in the payload | |
| this.props.saveComment(this.state.comment) | |
| this.setState({comment: ''}); | |
| } | |
| render() { | |
| return( | |
| <form onSubmit={this.handlesubmit.bind(this)} className="comment-box"> | |
| <textarea | |
| vallue={this.state.comment} | |
| onChange={this.handleChange.bind(this)}/> | |
| <button action="submit">Submit Comment</button> | |
| </form> | |
| ); | |
| } | |
| } | |
| export default connect(null, actions)(CommentBox) //first argument is reserved for mapStateToProps, so if we don't need state, use null | |
| // ------------------------------------------ | |
| // in src/components/comment_list | |
| import React, {Component} from 'react'; | |
| import { connect } from 'react-redux'; | |
| const CommentList => (props) => { | |
| const list = props.comments.map(comment => <li key={comment}>{comment}</li>); | |
| return( | |
| <ul className="comment-list">{list}</ul> | |
| ); | |
| } | |
| function mapStateToProps(state) { | |
| return { comments: state.comments }; | |
| } | |
| export default connect(mapStateToProps)(CommentList); | |
| // ------------------------------------------ | |
| // src/reducers/index.js | |
| import { combineReducers } from 'redux'; | |
| import commentsReducer from './comments' | |
| const rootReducer = combineReducers({ | |
| comments: commentsReducer | |
| }); | |
| export default rootReducer; | |
| // ------------------------------------------ | |
| // src/reducers/comments.js | |
| import { SAVE_COMMENT } from '../actions/types'; | |
| export default function(state = [], action) { | |
| switch(action.type) { | |
| case SAVE_COMMENT: | |
| return [...state, action.payload]; //equivalent to state.concat([action.payload]); | |
| } | |
| return state; | |
| } | |
| // ------------------------------------------ | |
| // test/reducers/comments_test.js | |
| import { expect } from '../test_helper'; | |
| import commentReducer from '../../src/reducers/comments'; | |
| import { SAVE_COMMENT } from '../../src/actions/types'; | |
| describe('Comments Reducer', () => { | |
| it('handles action with unknown type', () => { | |
| expect(commentReducer()).to.be.instanceof(Array); | |
| // or use eql (deep comparison), equal (check for absolute identical) | |
| // expect(commentReducer(undefined, {})).to.eql([]); //this is better cause it specifies that it's an empty array | |
| }); | |
| it('handles action of type SAVE_COMMENT', () => { | |
| const action = { type: SAVE_COMMENT, payload: 'new comment' }; | |
| expect( commentReducer([], action) ).to.eql(['new comment']); | |
| }) | |
| }) | |
| // ------------------------------------------ | |
| // src/actions/index.js | |
| import { SAVE_COMMENT } from './types'; | |
| export function saveComment(comment) { | |
| return { | |
| type: SAVE_COMMENT, | |
| payload: comment | |
| }; | |
| }; | |
| // ------------------------------------------ | |
| // src/actions/types.js | |
| export const SAVE_COMMENT = 'save_comment'; | |
| // ------------------------------------------ | |
| // test/actions/index_test.js | |
| import { expect } from ../test/helper; | |
| import { SAVE_COMMENT } from '../../src/actions/types'; | |
| import { saveComment } from '../../src/actions'; //inferring the index file | |
| describe('actions', () => { | |
| describe('saveComment', ()=> { | |
| it('has the correct type', ()=> { | |
| const action = saveComment(); | |
| expect(action.type).to.equal(SAVE_COMMENT); | |
| }); | |
| it('has the correct payload', ()=> { | |
| const action = saveComment('new Comment'); | |
| expect(action.payload).to.equal('new comment'); | |
| }); | |
| }); | |
| }); | |
| // ------------------------------------------ | |
| // in app_test.js | |
| import { renderComponent, expect } from '../test_helper'; | |
| import App from '../../src/components/app' | |
| // make sure commentbox and commentlist are displayed in the app | |
| describe('App', () => { | |
| let component; | |
| beforeEach(()=>{ | |
| component = renderComponent(App); | |
| }); | |
| it('shows a comment box', () => { | |
| expect(component.find('comment-box')).to.exist; | |
| }); | |
| it('shows a comment list', () => { | |
| expect(component.find('comment-list')).to.exist; | |
| }) | |
| }); | |
| // ------------------------------------------ | |
| // in comment_box_test.js | |
| import { renderComponent, expect } from '../test_helper'; | |
| import CommentBox from '../../src/components/comment_box'; | |
| describe('CommentBox', () => { | |
| let component; | |
| beforeEach(() => { | |
| component = renderComponent(CommentBox); | |
| }); | |
| it('has the correct class', () => { | |
| expect( component ).to.have.class('comment-box'); | |
| }) | |
| it('has a text area', () => { | |
| expect( component.find('textarea') ).to.exist; | |
| }); | |
| it('has a button', () => { | |
| expect( component.find('button') ).to.exist; | |
| }); | |
| // nesting closly related tests together in a nested describe | |
| describe('entering some text', () => { | |
| beforeEach(() => { | |
| component.find('textarea').simulate('change', 'new comment'); | |
| }); | |
| it('shows that text in the textarea', () => { | |
| expect(component.find('textarea')).to.have.value('new comment'); | |
| }); | |
| it('when submitted, clears the input', ()=> { | |
| component.simulate('sumbit'); // simulate the submit event - can only be used if the component is a form | |
| expect(component.find('textarea')).to.have.value(''); | |
| }) | |
| }); | |
| }); | |
| // ------------------------------------------ | |
| // in comment_list_test.js - with reducer & props | |
| import { renderComponent, expect } from '../test_helper'; | |
| import CommentList from '../../src/components/comment_list'; | |
| describe('CommentList', () => { | |
| let component; | |
| beforeEach(() => { | |
| const props = { comments: ['New Comment', 'Other New Comment'] }; | |
| component = renderComponent(CommentList, null, props); | |
| }); | |
| it('shows an LI for each comments', () => { | |
| expect( component.find('li').length ).to.equal(2); | |
| }); | |
| it('shows each comment that is provided', () => { | |
| expect(component).to.contain('New Comment'); | |
| expect(component).to.contain('Other New Comment'); | |
| }); | |
| }); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //useful for debugging json objects | |
| <pre><code>{JSON.stringify(theJsonObject, null, 4)}</code></pre> | |
| //JSON.stringify(object, replacer(mostly null), space) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment