Skip to content

Instantly share code, notes, and snippets.

@nonlogos
Last active February 15, 2017 00:06
Show Gist options
  • Select an option

  • Save nonlogos/80dc760c4182d90c248fb7e942544577 to your computer and use it in GitHub Desktop.

Select an option

Save nonlogos/80dc760c4182d90c248fb7e942544577 to your computer and use it in GitHub Desktop.
REACT NOTES
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>
);
};
};
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>
);
}
};
1. Build app with dummy data
2. replace dummy data with ajax
3. write middleware to help fetch data
// --------------------------------------
// 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
// 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
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);
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/
//
// 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);
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>
);
}
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}} />
}
/>
);
}
// 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);
}
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'));
// 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'));
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
};
// 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;
...
}
})
//
// 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
}
}
<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>
//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
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
//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>
);
//
// 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'}
]
}
//
// 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;
//
// 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
}
}
//
// 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 })
});
}
}
// ------------------------------------
// 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 };
// ---------------------------------------------
//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');
});
});
//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