over 3 years ago

簡易的使用 react-router-redux 建構 Redux 與 React Router 環境

React Router Tutorial 會一步一步教你怎使用相關的 Router 功能

延續 React Redux 環境建立及心得

再 Install react-router 跟 react-router-redux

npm install --save react-router
npm install --save react-router-redux

package.json 內容

{
  ...
  ....
  ......
  "dependencies": {
    "immutable": "^3.7.5",
    "react": "^0.14.2",
    "react-dom": "^0.14.2",
    "react-redux": "^4.0.0",
    "react-router": "^2.0.0",
    "react-router-redux": "^4.0.0",
    "redux": "^3.0.4",
    "webpack": "^1.12.6",
    "webpack-dev-server": "^1.12.1"
  },
  "devDependencies": {
    "babel-core": "^6.1.21",
    "babel-loader": "^6.2.0",
    "babel-preset-es2015": "^6.1.18",
    "babel-preset-react": "^6.1.18"
  }
}

修改 store.js
於 createStore 內加入 combineReducers 追加綁定 routing state

import { createStore, combineReducers } from 'redux';
import todos from './reducers/todos'
import { routerReducer } from 'react-router-redux'

export default createStore(
  combineReducers({
    todos,
    routing: routerReducer
  })
)

建立 \modules\Repos.js
此用 this.props.params 顯示
傳過來的 URL Params

import React from 'react'

export default React.createClass({
  render() {
    return <h2>{this.props.params.repoName}</h2>
  }
})

修改 \components\todoItem.jsx
使用 Link

import React from 'react'
import { Link } from 'react-router'

const todoItem = ({index, actions, value}) => (
  <div>
   <span onClick={e=>{
     actions.deleteTodo(index);
     alert(index);
   }}  id={value}>{value}
   </span>
   (<Link to={'/repos/' +  value}>Link</Link>)
   </div>
)

export default todoItem

修改 \components\todos.jsx
將 mapStateToProps 內的 todos 改為 state.todos

function mapStateToProps(state) {
  return {
    todos : state.todos
  }
}

修改 \components\app.jsx
建立 Router 結構使用 URL Params
使用 syncHistoryWithStore 關聯 browserHistory 跟 state.routing

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import store from '../store'
import Todos from './todos'
import { Router, Route, browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import Repos from '../modules/Repos'

const history = syncHistoryWithStore(browserHistory, store)
let reactElement = document.getElementById('react')
render(
  <Provider store={store}>
    <Router history={history}>
        <Route path="/" component={Todos} />
        <Route path="/repos/:repoName" component={Repos}/>
    </Router>
  </Provider>,
  reactElement
)

修改 package.json scripts 加入 --history-api-fallback

"scripts": {
    "start": "webpack-dev-server --devtool eval --progress --colors --hot --inline --history-api-fallback"
 },

執行

 npm start

執行結果
可點選 Link 顯示文字


也可以直接使用 URL Params 傳遞值

可以在 \reducers\todos.js
加入 LOCATION_CHANGE 在 Route 做轉換時來抓取或修改 state 內容

 import {
    LOCATION_CHANGE
} from 'react-router-redux';

switch(action.type)
  case LOCATION_CHANGE:
  ...
  .....
  .......
  retrun state
 
over 3 years ago

練習製作一個簡單的遮罩視窗 Component
內容如下

import React, {Component, PropTypes} from 'react'

class PopUp extends Component {
  constructor(props) {
    super(props);
    this.showPopUp = this.showPopUp.bind(this);
    this.resizePopUP = this.resizePopUP.bind(this);
    this.closePopUp = this.closePopUp.bind(this);
  }
  showPopUp() {
      this.refs.div.style.display = "block";
      this.refs.box.style.display = "inline-block";
      this.resizePopUP();
      window.addEventListener('resize', this.resizePopUP);
  }
  resizePopUP() {
      let box = this.refs.box;
      var boxWidth = box.offsetWidth;
      var boxheight = box.offsetHeight;

      box.style.zIndex = 100;
      box.style.position = "absolute";
      box.style.left = (window.innerWidth - boxWidth) / 2 + "px";
      box.style.top = (window.innerHeight - boxheight) / 2+ "px";
  }
  closePopUp() {
     this.refs.div.style.display = "none";
     this.refs.box.style.display = "none";
     window.removeEventListener('resize', this.resizePopUP);
  }
  render() {
    let divStyle = {"position":"absolute","display":"none","width":"100%","height":"100%","background-color":"#999999","left":"0px","top":"0px","opacity": "0.50","filter": "alpha(opacity=50)"};
    let boxStyle = {"display":"none"};
    return (
      <div>
      <div ref="div" style={divStyle} onClick={this.closePopUp}></div>
          <div ref="box" style={boxStyle}>
              {this.props.children}
          </div>
      </div>
    );
  }
}

export default PopUp

使用方式

包覆所要顯示的物件
這邊綁定了兩組 PopUp Component 採用 ref 做識別控制

可於主 View 呼叫 this.refs.PopUp2.showPopUp() 來顯示遮罩視窗
如 onClick={this.showBoxOne.bind(this)} 配置
點選遮罩可以關閉視窗
視窗會隨著畫面大小做置中變動

import React from 'react';
import ReactDOM from 'react-dom';
import PopUp from './Component/PopUp';
import ImgMask from './Component/imgMask';

class AppWrapper extends React.Component {
  constructor(props) {
    super(props)
  }
  showBoxTwo() {
    this.refs.PopUp2.showPopUp();
  }
  showBoxOne() {
    this.refs.PopUp.showPopUp();
  }
  render() {
    return(
      <div>
        <PopUp ref="PopUp">
          <div style={{"width":"150px","height":"180px","background-color":"white"}}>
          123
          465
          789
          </div>
        </PopUp>
        <input type="button" value="showBox" onClick={this.showBoxOne.bind(this)} />
        <PopUp ref="PopUp2">
        <ImgMask src="images/test.png" rectRadius={25} imgHeight={400} rectHeight={350}  rectWidth={250} />
        </PopUp>
        <input type="button" value="showBox2" onClick={this.showBoxTwo.bind(this)} />
      </div>
    )
  }
}
ReactDOM.render(<AppWrapper  />, document.getElementById('demo'))

效果


 
over 3 years ago

如何避免過度 re-render 底層 component

建立一個簡單的 component 來實做 re-render 情況

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  render() {
    return(
      <div>
      {
        this.props.list.map((item, i) => {
          return <p key={item+i}>{item}</p>
        })
      }
      </div>
    )
  }
}

以上 App Component會呈現 props.list 所有內容

為了避免 props.list 內容沒做更動就不需要 re-render Component
可利用 shouldComponentUpdate 來做判斷 return 是否要更新 Component

修正如下

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps.list !== this.props.list)
    return (nextProps.list !== this.props.list)
  }

  render() {
    return(
      console.log('component render');
      <div>
      {
        this.props.list.map((item) => {
          return <p key={item}>{item}</p>
        })
      }
      </div>
    )
  }
}

完成後需要一個外部的component來驗證 App Component 是否會因 props.list 差異而 re-render
外部的 component 建立兩個 button
一個更新 this.state.list 內容
另一個更新 this.state.otherList 內容
並將 this.state.list 傳遞給 App Component
內容如下

class AppWrapper extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
      otherList: [1, 2, 3, 4, 5]
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: [...this.state.list.slice(0, 9), 11]
    })
  }

  updateOtherList() {
    this.setState({
      otherList: [...this.state.otherList.slice(0, 4), 6]
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }> 
          Update List  
        </button>  
        <button onClick={ this.updateOtherList }> 
          Update Other List  
        </button> 
        <App list={ this.state.list } />
      </div>
    )
  }
}

結果


實驗結果會發現
每次按下 Update List 按鈕時 App Component 會出現 re-render
而 updateList() 是重複執行相同 array 內容給 this.state.list
但還是會造成 App 的 shouldComponentUpdate return true 而 re-render 產生 log ‘component render’

Update Other List 按鈕則是因為沒對 this.state.list 變動, 所以 shouldComponentUpdate return false

因為 React.Component 每當 state 做變動時都會產生一個 new state
而 js array 物件沒辦法做深層的判斷
所以造成 (nextProps.list !== this.props.list) return false
但一個一個去比對 array 內部 Object 是否相同, 實在是太費工了...

Immutable Data

Immutable Data 就是一旦創建,就不能再被更改的資料
對 Immutable 物件的任何修改或添加刪除操作都會返回一個新的 Immutable 物件
Immutable 實現的原理是 Persistent Data Structure(持久化資料結構)
也就是使用舊資料創建新資料時,要保證舊資料同時可用且不變
同時為了避免 deepCopy 把所有節點都複製一遍帶來的性能損耗
Immutable 使用了 Structural Sharing(結構共用)
即如果物件樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共用

使用 Immutable 優化 Component
修改如下

class AppWrapper extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      list: Immutable.List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
      otherList: Immutable.List.of(1, 2, 3, 4, 5)
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: this.state.list.set(9, 11)
    })
  }

  updateOtherList() {
    this.setState({
      otherList: this.state.otherList.set(4, 6)
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }>
          Update List
        </button>
        <button onClick={ this.updateOtherList }>
          Update Other List
        </button>
        <App list={ this.state.list } />
      </div>
    )
  }
}
ReactDOM.render(<AppWrapper  />, document.getElementById('img'))

結果


重新執行後

會發現 (nextProps.list !== this.props.list) 因 Immutable 而能使用 !== 判斷出差異
進而避免無謂的 re-render

而 Immutable 也提供 is API來做快速比對
Immutable.is(nextProps.list,this.props.list)

參考網站
https://github.com/camsong/blog/issues/3
http://aristid.es/optimizing-react-w-immutable/

 
over 3 years ago

練習一下 React Component 的編寫

圖片遮罩 component

import React, {Component, PropTypes} from 'react'

class ImgMask extends Component {
  constructor(props) {
    super(props)
    this._onLoad =  this._onLoad.bind(this);
  }
  _onLoad() {
    let imgDom = this.refs.imgDom;
    let range = imgDom.naturalHeight / this.props.imgHeight;
    let imgW = imgDom.naturalWidth / range;
    let imgH = imgDom.naturalHeight / range;
    let imgTop = (this.props.rectHeight - imgH) / 2;
    let imgLeft = (this.props.rectWidth - imgW) / 2;
    imgDom.style.top = imgTop + 'px';
    imgDom.style.left = imgLeft + 'px';
    imgDom.style.position = 'absolute';
  }
  render() {
    let divStyle = {"position" : "relative",
                    "overflow" : "hidden",
                    "width" : this.props.rectWidth,
                    "height" : this.props.rectHeight,
                    "display" : (this.props.isInlineBlock) ? "inline-block" : "block",
                    "borderRadius": this.props.rectRadius,
                    "borderStyle" : "solid",
                    "borderWidth" : this.props.borderWidth};
    return (
      <div style={divStyle}>
        <img ref="imgDom" src={this.props.src} onLoad={this._onLoad} height={this.props.imgHeight} />
      </div>
    );
  }
}

ImgMask.defaultProps  = {
  rectHeight: 200,
  rectWidth: 200,
  isInlineBlock:true,
  rectRadius: 20,
  borderWidth: 0,
  imgHeight: 275
}

ImgMask.propTypes = {
  rectHeight: PropTypes.number.isRequired,
  rectWidth: PropTypes.number.isRequired,
  isInlineBlock:PropTypes.bool.isRequired,
  rectRadius: PropTypes.number.isRequired,
  borderWidth: PropTypes.number.isRequired,
  imgHeight: PropTypes.number.isRequired,
  src: React.PropTypes.string.isRequired
}

export default ImgMask

使用方式

import React from 'react';
import ReactDOM from 'react-dom';
import ImgMask from './Component/imgMask'

class Main extends React.Component {
  render() {
    return (  
             <div>
                  <ul style={{display:"inline"}}>
                    <li><ImgMask src="images/test.png" rectRadius={50} /></li>
                    <li>
                      <ImgMask src="images/test.png" rectRadius={25} 
                        imgHeight={400} rectHeight={350}  rectWidth={250} />
                    </li>
                    <li><ImgMask src="images/test.png" rectRadius={10} borderWidth={5}  /></li>
                  </ul>
             </div>
           );
  }
}

ReactDOM.render(<Main  />, document.getElementById('img'))

效果

 
over 3 years ago

傳統的 React.createClass 跟 React.Component 宣告

React.createClass

const Pane = React.createClass({
  propTypes: {
    label: React.PropTypes.string.isRequired,
    children: React.PropTypes.element.isRequired
  },
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
});

React.Component

class Pane extends React.Component {
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
}
Pane.propTypes = {
  label: React.PropTypes.string.isRequired,
  children: React.PropTypes.element.isRequired
};

React v0.14.0 時所推出的 stateless components 宣告方式

stateless components

const Pane = (props) => <div>{props.children}</div>;

或者是 ES5 syntax

var Pane = function (props) {
  return <div>{props.children}</div>;
};

配上 ES6 syntax

const Pane = (props) => <div>{props.children}</div>;
Pane.propTypes = {
  label: React.PropTypes.string.isRequired,
  children: React.PropTypes.element.isRequired
};

比對 React.createClass 跟 React.Component 建構 Component
stateless components 的方式少了 React.xxx 也可省略 render 的呼叫
不過這種建構方式目前不支援相關的 lifecycle 函數
如果只是單純的接收 props 來直接配置 Component 內容的話
就可以考慮使用 stateless components 的方式

況且針對 state 的操作還是讓 main Component 去控制
和配置包覆內 stateless components 的 props 是最好不過了

參考網址
https://toddmotto.com/stateless-react-components/

 
over 3 years ago

比較 React.createClass 跟 ES6 modules => extends React.Component

建立 component classes 的語法差異

React.createClass

import React from 'react';

const Contacts = React.createClass({
  render() {
    return (
      <div></div>
    );
  }
});

export default Contacts;

React.Component

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div></div>
    );
  }
}

export default Contacts;

使用 ES6 語法 class 命名為 Contacts 和 extends React.Component
取代 React.createClass
執行 constructor 呼叫 super() 來傳遞 props 給 React.Component
且必須利用像 Babel 來轉換 ES6 語法到 ES5 以便在其他瀏覽器運作

propTypes 與 getDefaultProps

React.createClass

propTypes property 是個 Object 來宣告每個 prop 的配置條件
getDefaultProps property 是個 function 來 returns 一個 Object 來建立 initial props

import React from 'react';

const Contacts = React.createClass({
  propTypes: {

  },
  getDefaultProps() {
    return {
      
    };
  },
  render() {
    return (
      <div></div>
    );
  }
});

export default Contacts;

React.Component

propTypes property 可在 class 外執行 Contacts.propTypes 設定 prop 的配置條件
getDefaultProps 改為 defaultProps property
可在 class 外執行 Contacts.defaultProps 設定 initial props
相對能減少 Component 的基礎結構

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div></div>
    );
  }
}
Contacts.propTypes = {

};
Contacts.defaultProps = {

};

export default Contacts;

State 差異

React.createClass

利用 getInitialState function 來 return 一個 Object 來 initial states

import React from 'react';

const Contacts = React.createClass({
  getInitialState () {
    return {
      
    };
  },
  render() {
    return (
      <div></div>
    );
  }
});

export default Contacts;

React.Component

getInitialState function 被取消了
取而代之的是 constructor 內執行 this.state 來配置 initial states

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
    this.state = {

    };
  }
  render() {
    return (
      <div></div>
    );
  }
}

export default Contacts;

"this" 差異

React.createClass

使用 React.createClass 會自動 bind this -> React Component instance 到
如底下 onClick 範例宣告 this.handleClick
會直接配置 this -> React Component instance 到 handleClick function

import React from 'react';

const Contacts = React.createClass({
  handleClick() {
    console.log(this); // React Component instance
  },
  render() {
    return (
      <div onClick={this.handleClick}></div>
    );
  }
});

export default Contacts;

React.Component

而 ES6 有些差異, 他沒辦法自動 bind this -> React Component instance
如底下 onClick 範例宣告 this.handleClick
執行 handleClick function 呼叫 this 會出現 null

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    console.log(this); // null
  }
  render() {
    return (
      <div onClick={this.handleClick}></div>
    );
  }
}

export default Contacts;

有兩種方式可以 bind this -> React Component instance 到 function 內

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    console.log(this); // React Component instance
  }
  render() {
    return (
      <div onClick={this.handleClick.bind(this)}></div>
    );
  }
}

export default Contacts;

而最好的方式是直接在 constructor 內去做 bind
避免在 JSX 做 bind

import React from 'react';

class Contacts extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log(this); // React Component instance
  }
  render() {
    return (
      <div onClick={this.handleClick}></div>
    );
  }
}

export default Contacts;

Mixins

React.createClass

可以建立一個 Object Array 配置 mixins property
來擴充 component class property

import React from 'react';

var SomeMixin = {
  doSomething() {

  }
};
const Contacts = React.createClass({
  mixins: [SomeMixin],
  handleClick() {
    this.doSomething(); // use mixin
  },
  render() {
    return (
      <div onClick={this.handleClick}></div>
    );
  }
});

export default Contacts;

React.Component

可惜 Mixins 不支援 ES6 語法

內容來源
https://toddmotto.com/react-create-class-versus-component/

 
over 3 years ago

首先必需先安裝 NodeJS 和 npm

node 4.4.0
npm 3.3.12

安裝全域 mocha

npm install -g mocha

準備好底下 package.json 內容

{
  "name": "reacttest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "mocha --compilers js:babel-core/register ./test/* --require ./test/test_helper.js"
  },
  "author": "jasonwang",
  "license": "ISC",
  "dependencies": {
    "react": "^0.14.7",
    "react-dom": "^0.14.7"
  },
  "devDependencies": {
    "babel-core": "^6.4.5",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "chai": "^3.5.0",
    "jsdom": "^8.1.0",
    "mocha": "^2.4.5",
    "react-addons-test-utils": "^0.14.7"
  }
}

及 .babelrc 檔案

{
  "presets": ["es2015", "react"]
}

建立 Counter.js 內容是兩個 button 對 state 作加減

import React from 'react'
class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = {count : 0}
  }
  increment() {
    this.setState({count: ++this.state.count})
  }

  decrement () {
    this.setState({count: --this.state.count});
  }
render(){
    return(
      <div>
        <p><span>{this.state.count}</span></p>
        <button onClick={this.increment.bind(this)}>+</button>
        <button onClick={this.decrement.bind(this)}>-</button>
      </div>
    )
  }
}
export default Counter

建立 test\Counter-test.js 使用 chaireact-addons-test-utils

import React from 'react';
import ReactDOM from 'react-dom';

import TestUtils from 'react-addons-test-utils';
import Counter from '../Counter';
import {expect} from 'chai';

describe ('Counter', () => {
  // 測試 view 狀態
  it ('appears in view', () => {
    const shallowRenderer = TestUtils.createRenderer();
    shallowRenderer.render(<Counter className='counert' />);
    const result = shallowRenderer.getRenderOutput();
    expect(result.type).to.be.equal('div');
    //expect(result.props.className).to.be.equal('counert');
  });
  // 測試 button 狀態
  it('displays buttons', () => {
    const component = TestUtils.renderIntoDocument(<Counter />);
    const buttons = TestUtils.scryRenderedDOMComponentsWithTag(component,'button');
    expect(buttons.length).to.equal(2);
    expect(buttons[0].textContent).to.equal('+');
    expect(buttons[1].textContent).to.equal('-');
  });
  // 測試 + 動作
  it('adds when + is clicked', () => {
    const component = TestUtils.renderIntoDocument(<Counter />);
    const buttons = TestUtils.scryRenderedDOMComponentsWithTag(component,'button');
    expect(component.state.count).to.equal(0);
    TestUtils.Simulate.click(buttons[0]);
    expect(component.state.count).to.equal(1);
    TestUtils.Simulate.click(buttons[0]);
    expect(component.state.count).to.equal(2);
  });
  // 測試 - 動作
  it('subtracts when — is clicked', () => {
      const component = TestUtils.renderIntoDocument(<Counter />);
      const buttons = TestUtils.scryRenderedDOMComponentsWithTag(component,'button');
      expect(component.state.count).to.equal(0);
      TestUtils.Simulate.click(buttons[1]);
      expect(component.state.count).to.equal(-1);
      TestUtils.Simulate.click(buttons[1]);
      expect(component.state.count).to.equal(-2);
  });
  // 測試 - 和 + 動作
  it('adds and subtracts', () => {
      const component = TestUtils.renderIntoDocument(<Counter />);
      const buttons = TestUtils.scryRenderedDOMComponentsWithTag(component,'button');
      expect(component.state.count).to.equal(0);
      TestUtils.Simulate.click(buttons[0]);
      expect(component.state.count).to.equal(1);
      TestUtils.Simulate.click(buttons[1]);
      expect(component.state.count).to.equal(0);
   });

});

建立透過 jsdom 建立 DOM 需要的環境 test\test_helper.js

import jsdom from 'jsdom';

if (typeof document === 'undefined') {
    global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
    global.window = document.defaultView;
    global.navigator = global.window.navigator;
}

於 terminal 執行

npm test

package.json 指令設定

"scripts": {
    "test": "mocha --compilers js:babel-core/register ./test/* --require ./test/test_helper.js"
 },

--compilers js:babel-core/register 指定要轉碼的編譯器
./test/* 為測試的目錄及檔案
./test/test_helper.js DOM 需要的環境路徑

執行結果

  Counter
    √ appears in view
    √ displays buttons
    √ adds when + is clicked
    √ subtracts when — is clicked
    √ adds and subtracts


  5 passing (59ms)

參考網址
https://medium.com/viet.q.trang/basic-react-testing-walkthrough-b32b49c16a57#.jn6by86ag@
http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html
http://www.checkme.tw/wordpress/react-unit-testing/
https://facebook.github.io/react/docs/test-utils.html
http://chaijs.com/

 
over 3 years ago

首先必需先安裝 NodeJS 和 npm
準備好底下 package.json 內容並執行 npm install 安裝套件

{
  "name": "myredux",
  "version": "0.0.1",
  "description": "a todo app that you'd never want to use",
  "main": "index.js",
  "scripts": {
    "test": " ",
    "start": "webpack-dev-server --devtool eval --progress --colors --hot"
  },
  "keywords": [
    "test"
  ],
  "author": "Jason Wang",
  "license": "ISC",
  "dependencies": {
    "immutable": "^3.7.5",
    "react": "^0.14.2",
    "react-dom": "^0.14.2",
    "react-redux": "^4.0.0",
    "redux": "^3.0.4",
    "webpack": "^1.12.6",
    "webpack-dev-server": "^1.12.1"
  },
  "devDependencies": {
    "babel-core": "^6.1.21",
    "babel-loader": "^6.2.0",
    "babel-preset-es2015": "^6.1.18",
    "babel-preset-react": "^6.1.18"
  }
}

建立 webpack.config.js

var path = require("path");
module.exports = {
  entry: './components/app.jsx',
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules)/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  }
};

建立 index.html

<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
  <meta charset="UTF-8">
  <title>Todo</title>
</head>
<body>
  <div id="react"></div>
  <script src="bundle.js"></script>
</body>
</html>

建立 actions/index.js 編寫 action 設定

export function addTodo(todo) {
  return {
    type: 'addTodo',
    todo
  }
}

export function deleteTodo(index) {
  return {
    type: 'deleteTodo',
    index
  }
}

建立 reducers/todos.js 編寫 reducer 函數

import Immutable from 'immutable'

export default (state = Immutable.List(['Code More!']), action) => {
  switch (action.type) {
    case 'addTodo':
      return state.push(action.todo);
      case 'deleteTodo':
        return state.remove(action.index);
    default:
      return state;
  }
}

建立 store.js 執行 store createStore 將 actions 和 reducers 關聯在一起

import { createStore } from 'redux';
import todos from './reducers/todos'
export default createStore(todos)

建立 components/newTodo.jsx (dumb component) 執行新增Item

import React from 'react'

const newTodo = ({actions}) => (
  <div>
    <h3>New</h3>
    <input type="text" onKeyUp={
      e => {
        if(e.keyCode == 13){
          actions.addTodo(e.target.value);
          e.target.value = '';
        }
      }
    }/>
  </div>
)

export default newTodo

建立 components/todoItem.jsx (dumb component) 執行刪除Item

import React from 'react'

const todoItem = ({index, actions, value}) => (
   <p onClick={e=>{
     actions.deleteTodo(index);
     alert(index);
   }}  id={value}>{value}</p>
)

export default todoItem

建立 components/todos.jsx (smart component) 包含相關 dumb component
利用 connect 來監聽回傳一個新 store 的 state dispatch props 狀態
bindActionCreators 綁定 dispatch 到 action
直接供 dumb component 執行

import React , { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'

import NewTodo from './newTodo'
import TodoItem from './todoItem'
import * as TodoActions from '../actions'

class Todos extends Component {
  render() {
    const {todos, actions} = this.props;
    return (
      <div>
        <h1>Todos</h1>
        <NewTodo actions={actions}/>
        {todos.map((todo, index) => <TodoItem key={todo} index={index} actions={actions} value={todo} />)}
      </div>
    )
  }
}
function mapStateToProps(todos) {
  return {
    todos
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(TodoActions, dispatch)
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Todos)

建立 components/app.jsx 包含 Provider component 及 root-component

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import store from '../store'
import Todos from './todos'

let reactElement = document.getElementById('react')
render(
  <Provider store={store}>
    <Todos />
  </Provider>,
  reactElement
)

整個應用的 state 被存在在一棵 object tree 並且這個 object tree 只存在唯一一個 store 中
唯一改變 state 的方法就是 action
通常會在 action 呼叫 api

為了描述 action 如何改變 state tree 必須編寫 reducers
Reducer 只是一些纯函数,它接收先前的 state 和 action,並返回新的 state
其實很簡單,函數結構就是(previousState, action) => newState

Provider component
使用 Provider 包覆的 root component 才能使用 connect() 來獲得 stroe 的狀態

root component
將接收的 store 給底下 smart component 執行 connect() 來監聽回傳一個新 stroe 的 state dispatch props

smart component
1.透過 props 配置 action 給底下 dumb component 的 props
2.不會有自己的 CSS
3.不會產生任何的 Dom 只交由 dumb component 去產生
4.使用 classes 方式建構 component

dumb component
1.不會直接引用 actions
2.藉由 smart component 給予的 props action 做呼叫, 幫助執行各種 action 邏輯變化
3.擁有自身的 CSS
4.盡量使用 Stateless functional component 只處理 props, 除非你要使用 state 或其他 lifecycle hooks才改用 classes 方式建構 component

<Provider-component>
   <root-component>
     <smart-component>
        <dumb-component />
     </smart-component>  
   </root-component>    
</Provider-component>

Provider-component 跟 root-component 會放主要js 內
而 smart-component 跟 dumb-component 會被配置在 root-component js 內

大致流程
component ---> action ---> reducer ---> store ---> component

參考
http://rhadow.github.io/2015/07/30/beginner-redux/
https://reactjsnews.com/your-first-redux-app
http://redux.js.org/index.html
https://github.com/reactjs/react-redux/blob/master/docs/api.md

 
over 3 years ago

1.首先必需先安裝 NodeJSnpm

2.建立 app 資料夾

3.於 terminal app 資料夾路徑下執行

npm init 

輸入 package.json 相關初步設定

4.init 初始化後開始安裝必須套件

安裝 react

npm install --save react@0.14.7
npm install --save react-dom@0.14.7

安裝 webpack

npm install --save-dev webpack@1.12.12
npm install webpack-dev-server@1.12.1 -g

安裝 babel

npm install --save-dev babel-loader@6.2.1
npm install --save-dev babel-core@6.4.5
npm install --save-dev babel-preset-es2015@6.3.13
npm install --save-dev babel-preset-react@6.3.13

安裝完畢後可觀看 package.json 內容

{
  "name": "myappinit",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "jasonwang",
  "license": "ISC",
  "dependencies": {
    "react": "^0.14.7",
    "react-dom": "^0.14.7"
  },
  "devDependencies": {
    "babel-core": "^6.4.5",
    "babel-loader": "^6.2.1",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "webpack": "^1.12.12"
  }
}

只要有這 package.json 內容下次重新安裝只要執行

npm install 

就可以安裝 package.json 內所設定的套件

5.app 資料夾路徑下建立 React components

hello.jsx

import React from 'react';
import ReactDOM from 'react-dom';
 
class Hello extends React.Component {
  render() {
    return <h1>Hello</h1>
  }
}
 
ReactDOM.render(<Hello/>, document.getElementById('hello'));

world.jsx

import React from 'react';
import ReactDOM from 'react-dom';

class World extends React.Component {
  render() {
    return <h1>World</h1>
  }
}

ReactDOM.render(<World/>, document.getElementById('world'));

6.建立 main.js

import Hello from './hello.jsx';
import World from './world.jsx';

7.建立 webpack.config.js 設定檔案輸出格式
entry : 為主要 js
output : 為編譯後輸出的路徑(path)及js檔案(filename)
loaders : 設定 loader 的規格
test : 轉譯的檔案格式表示式
loader : 設定 loader 種類可用 ['xx','xx']
exclude: 過濾的目錄
query: 設定配置的轉譯語法

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: './main.js',
  output: { path: __dirname, filename: 'bundle.js' },
  module: {
    loaders: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react']
        }
      }
    ]
  },
};

7.建立 index.html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello React</title>
  </head>
  <body>
    <div id="hello"></div>
    <div id="world"></div>
    <script src="bundle.js"></script>
  </body>
</html>

8.於 terminal app 資料夾路徑下執行

webpack-dev-server --progress --colors

9.開啟 browsers 輸入 http://localhost:8080/webpack-dev-server/

可修改 package.json 內的 scripts
設定用 start 取代 webpack-dev-server --progress --colors

"scripts": {
  "build": "webpack",
  "start": "webpack-dev-server --devtool eval --progress --colors --hot"
}

直接執行

npm start

指令解釋
webpack-dev-server 會在 http://localhost:8080/webpack-dev-server/ 建立起專案的 server
devtool eval 會顯示出發生錯誤的行數與檔案名稱
progress 會顯示出打包的過程
colors 會幫 webpack 顯示的訊息加入顏色
webpack 執行編譯

參考來源
https://www.twilio.com/blog/2015/08/setting-up-react-for-es6-with-webpack-and-babel-2.html
http://rhadow.github.io/2015/04/02/webpack-workflow/

 
almost 4 years ago

資料來源 http://www.es6fiddle.net/

Arrow Functions

let square = x => x * x;
let add = (a, b) => a + b;
let pi = () => 3.1415;

console.log(square(5)); // 25
console.log(add(3, 4)); // 7
console.log(pi()); // 3.1415

Block Scope

var x = 0;

for (let i = 0; i < 10; i++) {
    x += 10;
}

try {
    console.log(i);
} catch(e) {
    console.log(
        'i does not exist here!'
    );
}

// show i does not exist here!

Classes and Inheritance

class Polygon {
    constructor(height, width) { //class constructor
        this.name = 'Polygon';
        this.height = height;
        this.width = width;
    }

    sayName() { //class method
        console.log('Hi, I am a', this.name + '.');
    }
}

class Square extends Polygon {
    constructor(length) {
        super(length, length); //call the parent method with super
        this.name = 'Square';
    }

    get area() { //calculated attribute getter
        return this.height * this.width;
    }
}

let s = new Square(5);

s.sayName(); // Hi, I am a Square.
console.log(s.area); // 25

可參考 http://www.codedata.com.tw/javascript/es6-4-maximally-minimal-classes/

Default Parameters

function sayMsg(msg='This is a default message.') {
    console.log(msg);
}

sayMsg(); // This is a default message.
sayMsg('This is a different message!'); // This is a different message!

Destructured Assignment

let [one, two] = [1, 2];
let {three, four} = {three: 3, four:  4};

console.log(one, two, three, four); // 1 2 3 4

Generators

function* range(start, end, step) {
    while (start < end) {
        yield start;
        start += step;
    }
}

for (let i of range(0, 10, 2)) {
    console.log(i);
}

// 0
// 2
// 4
// 6
// 8

可參考 http://www.codedata.com.tw/javascript/es6-3-generator/

Iterators

let arr = [1, 2, 3, 4, 5];
let sum = 0;

for (let v of arr) {
    sum += v;
}

console.log('1 + 2 + 3 + 4 + 5 =', sum); // 1 + 2 + 3 + 4 + 5 = 15

Map

let x = new Map([[1, 'is a number key']]);
let today = new Date()

//anything can be a key
x.set(today.toString(), 111)
x.set(today, 222);
x.delete(today.toString());

console.log('The map contains', x.size, 'elements.'); // The map contains 2 elements.
console.log('The map has a today Date key:', x.has(today)); // The map has a today Date key: true
console.log('The map has a today string key:', x.has(today.toString())); // The map has a today string key: false

//values and keys
x.forEach((value, key, map) => console.log(value, key, map)); 
// is a number key 1 [object Map]
// 222 Thu Jan 14 2016 10:34:11 GMT+0800 (台北標準時間) [object Map]

//iterable
for (let value of x) {
  console.log(value);
}
// 1,is a number key
// Thu Jan 14 2016 10:35:08 GMT+0800 (台北標準時間),222

//iterable values
for (let value of x.values()) {
  console.log(value);
}
// is a number key
// 222

//iterable keys
for (let value of x.keys()) {
  console.log(value);
}
// 1
// Thu Jan 14 2016 10:36:42 GMT+0800 (台北標準時間)

//iterable entries (key, value)
for (let value of x.entries()) {
  console.log(value);
}
// 1,is a number key
// Thu Jan 14 2016 10:37:10 GMT+0800 (台北標準時間),222

Promises

var longFn = function() {
    return new Promise(function(res, rej) {
        setTimeout(res, 1000);
    });
};

var coolFn = function() {
    console.log('cool');
};

// logs cool after 1 second
longFn().then(coolFn); // cool

可參考 https://davidwalsh.name/promises

Rest Parameters

function format(str, ...args) {
    return str.replace(/\{\s*(\d+)\s*\}/g, function(m, n) {
        return args[n];
    });
}

let msg = format(
    'The {0}st arg is a string, the {1} are {2}.',
    1,
    'rest',
    'unknown'
);

console.log(msg); // The 1st arg is a string, the rest are unknown.

Set

let x = new Set([1, 2, 3, 4, 4, 4, 5]);

x.add(6);
x.delete(2);

console.log('The set contains', x.size, 'elements.'); // The set contains 5 elements.
console.log('The set has 1:', x.has(1)); // The set has 1: true
console.log('The set has 8:', x.has(8)); // The set has 8: false

//values and keys are the same in a set
x.forEach((value, key, set) => console.log(value, key, set));
/*
1 1 [object Set]
3 3 [object Set]
4 4 [object Set]
5 5 [object Set]
6 6 [object Set]
*/

//iterable
for (let value of x) {
  console.log(value);
}
/*
1
3
4
5
6
*/
//iterable values
for (let value of x.values()) {
  console.log(value);
}
/*
1
3
4
5
6
*/
//iterable keys
for (let value of x.keys()) {
  console.log(value);
}
/*
1
3
4
5
6
*/
//iterable entries (key, value)
for (let value of x.entries()) {
  console.log(value);
}
/*
1,1
3,3
4,4
5,5
6,6
*/

Spread Operator

function add(a, b) {
    return a + b;
}

let nums = [5, 4];

console.log(add(...nums)); // 9

Template Literals

let person = {name: 'John Smith'};
let tpl = `My name is ${person.name}.`;

console.log(tpl); // My name is John Smith.