about 2 years ago

參考至 Function as Child Components
建立父項MyComponent

class MyComponent extends React.Component { 
  render() {
    return (
      <div>
        // 執行 children 函數 render 子項Component
        {this.props.children('Scuba Steve')}
      </div>
    );
  }
}
// 建立 props 函數
MyComponent.propTypes = {
  children: React.PropTypes.func.isRequired,
};

使用父項MyComponent

<MyComponent>
  {(name /* this.props.children('Scuba Steve') */) => (
    // 子項Component
    <div>{name}</div>
  )}
</MyComponent>

這樣的好處是可以避免 子項Component 的 props 命名衝突
因為 子項Component 可以各自針對自身的特性
對 父項Component 所傳遞的 props 參數值做選擇性接收
不一定要如同 HOC 那樣接收固定的 props 參數

轉換 HOC 至 Render prop 模式
原本的 HOC模式

const MenuHoc = (Component, functionId = 0) => class extends React.Component {
    render() {
      return (
        <Layout className="ant-layout-has-sider">
          <MenuSider functionId={functionId}/>
          <Layout>
            <MenuHeader />
            <MenuBreadcrumb functionId={functionId} />
            <Content style={{ padding: 10, background: '#fff', minHeight: window.innerHeight - 102 }}>
                  // 傳遞的子項Component
                  <Component {...this.props} />
            </Content>
          </Layout>
      </Layout>
      )
    }
}

使用 MenuHoc 方式, 這邊搭配 react-loadable 的 Loadable 做等待載入顯示

const HomeContainer = MeunHoc(
    Loadable({
        loader: () => import('./HomeContainer'),
        loading: MyLoadingComponent
    })
);

轉換為 Render prop 模式

class MenuFAC extends React.Component {
    render() {
      return (
        <Layout className="ant-layout-has-sider">
          <MenuSider functionId={this.props.functionId}/>
          <Layout>
            <MenuHeader />
            <MenuBreadcrumb functionId={this.props.functionId} />
            <Content style={{ padding: 10, background: '#fff', minHeight: window.innerHeight - 102 }}>
                // 執行 children 函數 render 子項Component
                {this.props.children(/*可自訂要傳遞給子項Component 的 props 參數*/)}
            </Content>
          </Layout>
      </Layout>
      )
    }
}

MenuFAC.propTypes = {
    children: PropTypes.func.isRequired,
};

使用 MenuFAC 方式

// 因為 Render 須為 Component 格式, 所以必須先執行 Loadable 來產生 Component
const UserManagementContainerLoader = Loadable({
    loader: () => import('./UserManagementContainer'),
    loading: MyLoadingComponent
});

const UserManagementContainer = () => {
    return <MenuFAC functionId={62} >
                {(/*接收父項傳遞過來的props變數*/) => ( 
                    <UserManagementContainerLoader /*配置所要的props值 如 title={props.title}*/ />
                )}
           </MenuFAC>
};

進階的用法可參考
I’m Breaking up with Higher Order Components.

不過這種模式還是存在一些爭議XD....

參考來源
Function as Child Components
Use a Render Prop!

 
about 2 years ago

在 Router 切換路徑時會發生

Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.

主要是在執行 Ajax 執行 response 函數時
因為 component 已經 unmounted
導致 setState 執行時所造成的錯誤

官方有推薦的做法
https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

而消極的做法

constructor(props) {
      super(props);
      const CancelToken = axios.CancelToken;
      this.isUnmounted = false; // 建立一個tag 來監測 component 是否已經 unmounted
}
...
..
componentWillUnmount() {
    this.isUnmounted = true;
}
...
_processResponse(response) {
  if(this.isUnmounted) { // 檢視是否要執行 setState
    return;
  }
  this.setState({
    visible: false,
  });
}

或使用 axios CancelToken 搭配 Hoc 來取消 request

const AxiosHoc = (Component) => class extends React.Component {
    constructor(props) {
      super(props);
      const CancelToken = axios.CancelToken;
      this.source = CancelToken.source();
    }  
    _get = (url, params, thenFunction) => {
      axios.get(url, { params : params, cancelToken: this.source.token })
      .then(thenFunction).catch(this._errorCatch);
    }
    _post = (url, params, thenFunction) => {
      axios.post(url, Object.assign(params, { cancelToken: this.source.token }))
      .then(thenFunction).catch(this._errorCatch);
    }
    _put = (url, params, thenFunction) => {
      axios.put(url, Object.assign(params, { cancelToken: this.source.token }))
      .then(thenFunction).catch(this._errorCatch);
    }
    _delete = (url, params, thenFunction) => {
      axios.delete(url, { params : params, cancelToken: this.source.token })
      .then(thenFunction).catch(this._errorCatch);
    }    
    _errorCatch = (error) => {
      if (axios.isCancel(error)) {
        console.log(error.message);
      } else {
        console.log(error);
      }
    }
    componentWillUnmount() {
        this.source.cancel(`${Component.name} Request Operation canceled by source`);
    }    
    render() {
      return <Component {...this.props} {...this.state}
              get={this._get}
              post={this._post}
              put={this._put}
              delete={this._delete}
               />
    }
  }

使用方式

class UserEditForm extends Component {
...
..
.
    addUser = () => {
        this.props.post('users', this.state.values, this._addResponse);
    }
    _addResponse = (response) => {
    ...
    ..
    }
}
export default AxiosHoc(UserEditForm);
 
over 2 years ago

step 1

create-react-app code_splitting_router

step 2

cd code_splitting_router

step 3

yarn add react-router-dom

step 4
建立 components/AsyncComponent

import React, { Component } from 'react';

export default function asyncComponent(importComponent) {

  class AsyncComponent extends Component {

    constructor(props) {
      super(props);

      this.state = {
        component: null,
      };
    }

    async componentDidMount() {
      const { default: component } = await importComponent();

      this.setState({
        component: component
      });
    }

    render() {
      const C = this.state.component;

      return C
        ? <C {...this.props} />
        : null;
    }

  }

  return AsyncComponent;
}

step 5
建立 containers/Home, NotFound, Posts

//containers/Home.js
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <div>
        Home
      </div>
    );
  }
}

export default Home;
//containers/NotFound.js
import React, { Component } from 'react';

class NotFound extends Component {
  render() {
    return (
      <div>
        NotFound
      </div>
    );
  }
}

export default NotFound;
//containers/Posts.js
import React, { Component } from 'react';

class Posts extends Component {
  render() {
    return (
      <div>
        Home
      </div>
    );
  }
}

export default Posts;

step 6
修改 index.js

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import asyncComponent from './components/AsyncComponent';

const AsyncHome = asyncComponent(() => import('./containers/Home'));
const AsyncPosts = asyncComponent(() => import('./containers/Posts'));
const AsyncNotFound = asyncComponent(() => import('./containers/NotFound'));

ReactDOM.render(
<BrowserRouter>
  <Switch>
    <Route path="/" exact component={AsyncHome} />
    <Route path="/posts/:id" exact component={AsyncPosts} />
    <Route component={AsyncNotFound} />
  </Switch>
</BrowserRouter>
  ,
  document.getElementById('root')
);

step 7

npm run build

效果
code_splitting_1

step 8

npm start

效果
code_splitting_2

或使用 react-loadable
step 1

yarn add react-loadable

step 2
修改index.js

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Loadable from 'react-loadable';

const MyLoadingComponent = ({isLoading, error}) => {
  // Handle the loading state
  if (isLoading) {
    return <div>Loading...</div>;
  }
  // Handle the error state
  else if (error) {
    return <div>Sorry, there was a problem loading the page.</div>;
  }
  else {
    return null;
  }
};

const AsyncHome = Loadable({
  loader: () => import('./containers/Home'),
  loading: MyLoadingComponent
});

const AsyncPosts = Loadable({
  loader: () => import('./containers/Posts'),
  loading: MyLoadingComponent
});

const AsyncNotFound = Loadable({
  loader: () => import('./containers/NotFound'),
  loading: MyLoadingComponent
});

ReactDOM.render(
<BrowserRouter>
  <Switch>
    <Route path="/" exact component={AsyncHome} />
    <Route path="/posts/:id" exact component={AsyncPosts} />
    <Route component={AsyncNotFound} />
  </Switch>
</BrowserRouter>
  ,
  document.getElementById('root')
);

參考來源
http://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#code-splitting
https://facebook.github.io/react/blog/2017/05/18/whats-new-in-create-react-app.html#code-splitting-with-dynamic-import

 
over 2 years ago

看個例子

public int square(int x) {
    return x * x;
}

上述 square 函數提供了一組 input 接收參數 int x
也清楚描述 output return int

再看下個例子

public void processNext() {
    Message message = InboxQueue.popMessage();

    if (message != null) {
        process(message);
    }
}

這邊隱藏了 input InboxQueue 的 popMessage()
也隱藏了 output process 的處理結果

在不清楚 InboxQueue 的行為下
只看 processNext 函數名稱
其實也猜不出內部所執行的行為
且隱藏了 output 也難猜出內部所執行的結果

而這樣的 hidden inputs 和 outputs 稱之為 side-effects

再看底下例子

public Program getCurrentProgram(TVGuide guide, int channel) {
  Schedule schedule = guide.getSchedule(channel);

  Program current = schedule.programAt(new Date());

  return current;
}

這隱藏了 input new Date()

將 new Date() 提出作為參數

public Program getProgramAt(TVGuide guide, int channel, Date when) {
  Schedule schedule = guide.getSchedule(channel);

  Program program = schedule.programAt(when);

  return program;
}

雖然將兩個參數改為三個參數
看似複雜但也清楚表達了這函數所要執行的所有職責
看 input 三個參數 跟 函數名稱 就大概清楚知道 output Program 的內容

雖然不可能完全避免掉 side-effects 但至少在 coding 過程中
需避免減少這類函數功能描述不清 (做了哪些事 影響到那些外部資源) 的情況發生

來源網址
http://blog.jenkster.com/2015/12/what-is-functional-programming.html

 
over 2 years ago

在官方的文件有提出 State Updates May Be Asynchronous

所以在處理連續的運算式時建議還是使用 callback 的方式處理 setState 以避免 非同步所造成的資料錯亂

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

底下網址有針對 setState 有更深入的分析
https://medium.com/javascript-scene/setstate-gate-abc10a9b2d82#.ertzgfsqi
https://medium.freecodecamp.com/functional-setstate-is-the-future-of-react-374f30401b6b#.2kom0bnyb

 
over 2 years ago

手動於 https://nodejs.org/en/download/current/ 下載最新的 Node 來更新實在是很煩人

安裝 nvm 來控管自己電腦開發端的 Node 版本

安裝後

於 cmd 中執行 nvm 指令

觀看電腦 Node 的版本列表跟目前使用的 Node 版本

nvm list

安裝 Node

nvm install <version> [arch]
例如 : 安裝 Node 6.3.0 64位元 => nvm install 6.3.0 64

切換 Node 版本

nvm use <version> [arch]
例如 : nvm use 6.3.0 64

移除指定版本

nvm uninstall <version>
 
over 2 years ago

先準備好CSS圖片
rateit.css

.rateit-bg-star{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxhYWdfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8zXyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIwLjIwNyIgeTE9IjgiIHgyPSIxNS43OTIiIHkyPSI4Ij4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojQ0NDQ0NDIi8+DQoJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0U2RTZFNiIvPg0KPC9saW5lYXJHcmFkaWVudD4NCjxwYXRoIGZpbGw9InVybCgjU1ZHSURfM18pIiBkPSJNOC4zNjcsMC44MTRsMS45ODQsNC42OTFsNS4wNzQsMC40MzRjMC4zNTIsMC4wMzEsMC40OTYsMC40NzMsMC4yMjcsMC43MDNsLTMuODQ4LDMuMzM2bDEuMTUyLDQuOTYxDQoJYzAuMDgyLDAuMzQ0LTAuMjkzLDAuNjE3LTAuNTk0LDAuNDM0TDgsMTIuNzRsLTQuMzYzLDIuNjMzYy0wLjMwMSwwLjE4LTAuNjc2LTAuMDktMC41OTQtMC40MzRsMS4xNTItNC45NjFMMC4zNDQsNi42NDINCglDMC4wNzgsNi40MDgsMC4yMjMsNS45NywwLjU3NCw1LjkzOWw1LjA3NC0wLjQzNGwxLjk4NS00LjY5MUM3Ljc2OSwwLjQ4Niw4LjIzLDAuNDg2LDguMzY3LDAuODE0TDguMzY3LDAuODE0eiBNOC4zNjcsMC44MTQiLz4NCjwvc3ZnPg0K)}

.rateit-selected{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxhYWdfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8zXyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIwLjIwNyIgeTE9IjgiIHgyPSIxNS43OTIiIHkyPSI4Ij4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojRkJCMDNCIi8+DQoJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0ZGOTQyRCIvPg0KPC9saW5lYXJHcmFkaWVudD4NCjxwYXRoIGZpbGw9InVybCgjU1ZHSURfM18pIiBkPSJNOC4zNjcsMC44MTRsMS45ODQsNC42OTFsNS4wNzQsMC40MzRjMC4zNTIsMC4wMzEsMC40OTYsMC40NzMsMC4yMjcsMC43MDNsLTMuODQ4LDMuMzM2bDEuMTUyLDQuOTYxDQoJYzAuMDgyLDAuMzQ0LTAuMjkzLDAuNjE3LTAuNTk0LDAuNDM0TDgsMTIuNzRsLTQuMzYzLDIuNjMzYy0wLjMwMSwwLjE4LTAuNjc2LTAuMDktMC41OTQtMC40MzRsMS4xNTItNC45NjFMMC4zNDQsNi42NDINCglDMC4wNzgsNi40MDgsMC4yMjMsNS45NywwLjU3NCw1LjkzOWw1LjA3NC0wLjQzNGwxLjk4NS00LjY5MUM3Ljc2OSwwLjQ4Niw4LjIzLDAuNDg2LDguMzY3LDAuODE0TDguMzY3LDAuODE0eiBNOC4zNjcsMC44MTQiLz4NCjwvc3ZnPg0K)}
[class^=rateit-] {
  width: 16px;
  height: 16px
}

RateitComponent.js

import React, { Component } from 'react';
import './rateit.css';

class Rateit extends Component {
  constructor(props) {
    super(props);
    this.state = { selectedValue: this.props.selectedValue };
  }
  _processRateit(selectedValue) {
    for(let i = 1; i <= this.props.length; i++) {
      (selectedValue >= i) ?
        this.refs[i].classList.add("rateit-selected"):
        this.refs[i].classList.remove("rateit-selected");
    }
  }
  _onClick(selectedValue) {
    const setValue = (this.state.selectedValue === selectedValue) ? 0 : selectedValue;
    this._processRateit(setValue);
    this.setState({ selectedValue : setValue });
  }
  getSelectedValue() {
    return this.state.selectedValue;
  }
  render() {

    const {length, order} = this.props;

    let stars = [];

    for(let i = 1; i <= length; i++) {
      let className = (this.state.selectedValue >= i) ? 
                                    'rateit-bg-star rateit-selected':'rateit-bg-star';
      stars.push(
          <div key={i} className={className} ref={i}
          onMouseOver={this._processRateit.bind(this, i)}
          onMouseOut={this._processRateit.bind(this, this.state.selectedValue)}
          onClick={this._onClick.bind(this, i)}
          />
        );
    }

    const containerStyle = { 'display': 'flex',
                             'justifyContent': (order === 'ASC') ? 'flex-start' : 'flex-end',
                             'flexDirection':  (order === 'ASC') ? 'row'        : 'row-reverse' };
    return (
      <div style={containerStyle}>
        {stars}
      </div>
    )
  }
}
// 配置預設 props 
Rateit.defaultProps = {
  order: 'ASC', // 遞增或遞減
  length: 5,    // 星星長度
  selectedValue: 0 // 預設選取值
}
// 配置 propTypes 
Rateit.propTypes  = {
  order: React.PropTypes.oneOf(['ASC', 'DESC']),
  size:React.PropTypes.number,
  length: React.PropTypes.number,
  selectedValue: React.PropTypes.number
}

export default Rateit;

使用方式

import React, { Component } from 'react';
import Rateit from './Components/RateitComponent';

class App extends Component {
  _Send(e) {
    console.log(`RateitOne : ${this.refs.RateitOne.getSelectedValue()}`);
    console.log(`RateitTwo : ${this.refs.RateitTwo.getSelectedValue()}`);
  }
  render() {
    return (
      <div className="App">
        <Rateit ref="RateitOne" length={10} />
        <Rateit ref="RateitTwo" order={'DESC'}/>
        <button onClick={this._Send.bind(this)}>Send</button>
      </div>
    );
  }
}

export default App;

效果
RateitComponent

 
almost 3 years ago

範例如下

class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {
      distributionMap : [],
      showMap : true,
      ...
      ..
    };
  }
  ...
  ..
  render() {
    return(
      <div>
      { // 使用 立即呼叫函式 來執行複雜的判斷式
       (() => {
          if (this.state.distributionMap && this.state.showMap) {
            return <DistributionMap data={this.state.distributionMap} />;
          }
        })()
      }
      <ToDo data={this.state.todo} />
      <IncompleteItems data={this.state.incompleteItems} />
      </div>
    );
  }
}

使用 立即呼叫函式 雖然會影響效能
不過在執行比較複雜的判斷情境下
或許可以考慮犧牲些效能來換取程式的可讀性

 
almost 3 years ago
if (isMonday) {
    a = 1;
} else if (isSaturday || isSunday) {
    a = 0;
} else {
    a = -1
}

可修改為

let isWeekend = (isSaturday || isSunday);
a =   (isMonday)  ? 1 
    : (isWeekend) ? 0 
    : -1;

將多個狀態判斷式合併為一個判斷結果
並給予適合的變數名稱
再利用排版將 ? 跟 : 做段落區分

 
about 3 years ago

使用 create-react-app 來迅速建立開發環境

npm install -g create-react-app

建立開發專案資料夾

create-react-app react-mobx-todo-editor

由於預設的環境不支援 decorators 裝飾模式
需手動配置 Babel plugin
先執行 create-react-app eject 指令來解除 CLI 環境

npm run eject

對於 create-react-app 的內部結構及相關指令可參考底下連結
http://www.eloquentwebapp.com/create-react-apps/

安裝 Babel plugin decorators 裝飾模式

npm install babel-plugin-transform-decorators-legacy --save-dev

修改 create-react-app eject 後的 config 檔案
config/babel.dev.js 及 config/babel.prod.js
於 plugins 屬性內追加底下設定

plugins: [
    // handles @decorator
    require.resolve('babel-plugin-transform-decorators-legacy'),

測試 create-react-app 環境

npm run start

會自動導入一個瀏覽器頁面並開啟 development server 網址

安裝 mobx-react

npm install mobx mobx-react --save

建立 mobx model
Todo.js

import {observable, computed} from 'mobx'

// 建立 Todo 流水號
var _nextId = 0
function nextId(){ _nextId++; return _nextId }

// todo model
export class Todo{
    
    id = nextId();
    // 使用 @observable 來觀察變數的變化
    @observable text = '';
    @observable done = false;

    // 當 observable 的變數有變動時會執行底下函數
    @computed get isValid(){
        return this.text !== '';
    }
    
    // 建立 {} 物件
    serialize(){
        return {
            id: this.id,
            text: this.text,
            done: this.done
        }
    }
    
    // 解構 json 至 todo 物件
    static deserialize(json: Object){
        const todo = new Todo()
        todo.id = json['id'] || nextId()
        todo.text = json['text'] || ''
        todo.done = json['done'] || false
        return todo
    }    
}

建立 ViewModel
ViewModel.js

import {observable, action} from 'mobx'
import {Todo} from './Todo'

export class TodoViewModel{
    // 使用 @observable 來觀察變數的變化
    @observable todos = []

    // 使用建構式初始化 todos
    constructor(){
        this.load()
    }

    // 使用 @action 來綁定 event
    // 建立一組新的todo
    @action
    add(){
        const newTodo = new Todo()
        this.todos.push(newTodo)
        return newTodo
    }

    // 移除 todo
    @action
    remove(todo: Todo){
        // 移除 todo
        const index = this.todos.indexOf(todo)
        if(index > -1){
            this.todos.splice(index, 1)
        }
    }

    // 載入 window.localStorage 所記錄的 todos json
    @action
    load(){
        if(window.localStorage){
            const json = JSON.parse(window.localStorage.getItem("todos") || "[]")

            // 使用 Todo.deserialize 來解析 window.localStorage 所記錄的 todos json
            this.todos = json.map(todo => Todo.deserialize(todo))
        }
    }

    // 儲存所有 todos
    @action
    save(){
        // 使用 todo.isValid 來驗證 todo 格式是否正確
        if(this.todos.filter(todo => todo.isValid === false).length > 0){
            alert("Unable to save: There are invalid Todos.")
        }
        
        // 儲存所有 todos 至 window.localStorage
        // 使用 todo.serialize() 轉換到 {} 物件再利用 JSON.stringify 轉成 JSON 格式
        if(window.localStorage){
            window.localStorage.setItem(
                "todos", 
                JSON.stringify(
                    this.todos.map(todo => todo.serialize())
                )
            )
        }
    }
}

建立 TodoView.js

import React from 'react'
import {observer} from 'mobx-react'

// 使用 @observer 來觀察 model(TodoViewModel) 的動態
@observer
export class TodoView extends React.Component{
    
    render(){
        const model = this.props.model

        // 使用 props.model(TodoViewModel) @action 所定義的 event 來操作 model(TodoViewModel) 內的資料
        // 使用 props.model.todos 來傳遞 todo 給子view 呈現資料
        return <div>
            <h1>React & MobX Todo List!</h1>
            <p>
                <button onClick={() => model.add()}>New Todo</button>
                <button onClick={() => model.load()}>Reload Todos</button>
                <button onClick={() => model.save()}>Save Todos</button>
            </p>
            {model.todos.map((todo, i) => <SingleTodoView key={todo.id} model={model} todo={todo} />)}
        </div>
    }
}

// 使用 @observer 來觀察 model(Todo) 的動態
@observer
export class SingleTodoView extends React.Component{

    render(){
        const model = this.props.model
        const todo = this.props.todo
        
        // 使用 props.model(TodoViewModel) @action 所定義的 event 來操作 model(TodoViewModel) 內的資料
        // 使用 props.todo(Todo) 來配置 todo view 的內容
        return <p>
                    #{todo.id} 
                    <strong>{todo.text}</strong> 
                    <i>{todo.done ? 'DONE!' : ''}</i>
                    
                    <br/>

                    <input type="checkbox" checked={todo.done} onChange={e => {todo.done = e.target.checked}} />
                    <input type="text" value={todo.text} onChange={e => {todo.text = e.target.value}} />
                    <button onClick={() => model.remove(todo)}>Delete</button>
                </p>
    }
}

建立 index.js

import React from 'react';
import {render} from 'react-dom';
import {TodoView} from './TodoView';
import {TodoViewModel} from './TodoViewModel';

// 建立 TodoViewModel
const model = new TodoViewModel();

// 將 TodoViewModel 傳入 TodoView
render(<TodoView model={model} />, document.getElementById('root'));

檢視結果

npm run start

開發流程
1.Todo 包含一組todo 的基本資料欄位及檢查運算式 isValid 和資料格式轉換 serialize 及 deserialize
2.TodoViewModel 的 todos 包含了所有的 Todo 資料, 並建立處理 todos 資料的 event
3.TodoView 的 TodoView Component 處理 TodoViewModel 內的 todos 資料及event
4.TodoView 的 SingleTodoView Component 接收及觀察 Todo 內的資料, 並使用 TodoViewModel.remove(todo) 刪除自身資料
5.index.js 建立 TodoViewModel 並傳遞給 TodoView 做 render

更詳細的 mobx MVVM 作法請參考官方網址
https://github.com/mobxjs/mobx

參考網址
https://medium.com/MattiaManzati/building-a-react-mobx-application-with-mvvm-ec0b3e3c8786#.rwee0jx8y@