LoginSignup
7

More than 5 years have passed since last update.

mobxの練習

Posted at

React のデータ管理をmobxにしようと思い。練習用に。
incrementとかのとても分かりやすいサンプルはたくさんあるので、もうちょいアプリケーションっぽいものを。
qiitaのAPIから記事データを取得して、記事情報/お気に入り情報をobservable。

成果物

デモ
https://mobx-practice-v1.netlify.com/

コード
https://github.com/nakadoribooks/mobx-practice/releases/tag/v0.0.1

流れ

データをフェッチして
itemList と likedList を computed で参照。
like でお気に入り登録、isLiked を observable。

IMG_0422.jpg

環境

React / Mobx / StyledComponent

今回StyledComponentはあんま関係ないので、React/Mobxの部分だけをご紹介。

デコレータの扱いが微妙でハマった。
結果、こんな感じのpackage.json と webpack.config

package.json
{
  "name": "mobx",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "babel-eslint": "^8.2.1",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-mobx": "^1.0.2",
    "babel-preset-stage-1": "^6.24.1",
    "mobx": "^3.5.1",
    "mobx-react": "^4.4.2",
    "netlify-cli": "^1.2.2",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-router-dom": "^4.2.2",
    "styled-components": "^3.1.6",
    "superagent": "^3.8.2"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "eslint": "^4.18.0",
    "eslint-plugin-react": "^7.6.1",
    "webpack": "^3.11.0",
    "webpack-dev-server": "^2.11.1"
  },
  "babel": {
    "plugins": [
      "transform-decorators-legacy",
      "transform-class-properties",
      "transform-runtime"
    ]
  },
  "scripts": {
    "start": "webpack-dev-server --history-api-fallback",
    "build": "webpack -d && cp dist/index.html dist/404.html"
  }
}

webpack.config.js
const path = require('path');

module.exports = {
    entry: path.resolve(__dirname, 'src/app.js'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'app.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                    plugins: [
                        'transform-decorators-legacy',
                        'transform-class-properties'
                    ],
                    presets: ['react', 'env']
                }
            }
        ]
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        port: 3000
    },
    devtool: 'inline-source-map'
};

実装

実装見ていきます。

ファイル構成

  • component/item/index (記事一覧画面)
  • component/like/index (いいねした記事画面)
  • component/mypage/index (マイページ)
  • store/entity/item (記事情報)
  • store/ItemStore (記事ストア)
  • app (ストアをコンポーネントにアサイン)

スクリーンショット 2018-02-25 20.12.46.png

ItemStore

主に以下3つ。

  • fetch データ取ってくる
  • itemList (computed)
  • likedList (computed)
ItemStore.js
import {observable, action, computed} from 'mobx';
import superagent from 'superagent';
import Item from './entity/Item';
import Config from '../Config';

class ItemStore{

    _loadedData = false

    @observable _data = null;
    @observable selectedItem = null
    @observable selectedItemLiked = null

    @computed get itemList() { return this._data || [];}
    @computed get likedList() {
        return this.itemList
            .filter( elem => { return elem.isLiked; })
            .sort((e1, e2)=>{ return e1.editedAt < e2.editedAt; });
    }

    @action select(itemId){
        this.selectedItem = this.find(itemId);
    }

    @action selectLiked(itemId){
        this.selectedItemLiked = this.find(itemId);
    }

    find(id) {
        if(id == null){ return null; }
        return this.itemList.find( item => { return (item.data.id == id); });
    }

    fetchIfneeded(){
        if(this._loadedData){ return; }
        this._loadedData = true;
        this._fetch();
    }
    _fetch(){
        superagent
            .get(`${Config.apiEndpint}/items`).set('Authorization', `Bearer ${Config.token}`)
            .end((error, res) => {
                if (res.status != 200) {
                    return;
                }
                const json = JSON.parse(res.text);
                const data = json.map((item) => { return new Item(item); });
                this._data = data;
            });
    }
}

export default ItemStore;

Item

記事一個分のデータ。

  • お気に入り状態の保持
store/entity/Item.js
import {observable, action} from 'mobx';

class Item{

    constructor(data){
        this.data = data;
    }

    editedAt = new Date()

    @observable isLiked = false

    @action like(){
        this.isLiked = true;
        this.editedAt = new Date();
    }

    @action unLike(){
        this.isLiked = false;
        this.editedAt = new Date();
    }

}

export default Item;

App

stores(ストアの集合) を Providor(mobxとreactつなぐやつ?)に渡してその中に各コンポーネント(画面)を定義

App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route} from 'react-router-dom';
import {Item, Like, Mypage} from './components/';
import { Provider } from 'mobx-react';
import {ItemStore,UserStore} from './store';
import {Menu, MenuItem} from './style/Menu';

const stores = {
    itemStore: new ItemStore()
    , userStore: new UserStore()
};

class App extends React.Component {

/* 〜 ルーティングとか、略 〜 */

    render() {
        return (
            <BrowserRouter>
                <div>
                    <Menu state={this.state}>
                        <MenuItem to="/item" onClick={this.onClickMenu.bind(this)} data-index={1}>New</MenuItem>
                        <MenuItem to="/like" onClick={this.onClickMenu.bind(this)} data-index={2}>Like</MenuItem>
                        <MenuItem to="/mypage" onClick={this.onClickMenu.bind(this)} data-index={3}>MyPage</MenuItem>
                    </Menu>
                    <Provider {...stores}>
                        <div>
                            <Route exact path="/" component={Item} />
                            <Route path="/item/:id?" component={Item} />
                            <Route path="/like/:id?" component={Like} />
                            <Route path="/mypage" component={Mypage} />
                        </div>
                    </Provider>
                </div>
            </BrowserRouter>
        );
    }
}

ReactDOM.render(
    <App />,
    document.getElementById('app')
);

Item 画面

記事一覧画面。

Providerに渡されてるitemStore を使う宣言。

@inject('itemStore')
@observer
  • 画面全体のdidMount で fetchする
  • componentWillReceiveProps で画面遷移を拾って対象の記事を選択状態にする
  • お気に入りボタンタップ(onClickLike)で like / unlike
item/index.js

import React from 'react';
import PropTypes from 'prop-types';
import {inject, observer} from 'mobx-react';
import {
    Loader
    , ListContents, ListItemWrapper, ListItemRow, ListItemLink, ListLikeImage
    , ContentsWrapper, ContentsInner, Title, FakeTitle, LikeButton} from '../../style/Common';


/**
 * 右の記事内容の画面
 */
@inject('itemStore')
@observer
class Contents extends React.Component {

    /* 〜 略 〜 */

    onClickLike(){
        const item = this.props.itemStore.selectedItem;
        if(item.isLiked){
            item.unLike();
        }else{
            item.like();
        }
    }

    render() {

        const item = this.props.itemStore.selectedItem;

        return (
            /* 〜 略 〜 */
        );
    }
}

/**
 * 左のリスト
 */
@inject('itemStore')
@observer
class List extends React.Component {
    /* 〜 略 〜 */
}

/**
 * ItemComponent
 * メイン
 */

@inject('itemStore')
@observer
class ItemComponent extends React.Component {

    /* 〜 略 〜 */

    componentDidMount(){
        this.props.itemStore.fetchIfneeded();
    }

    componentWillReceiveProps(nextProps){
        const id = nextProps.match.params.id;
        this.props.itemStore.select(id);
    }

    render() {
        return (
            <div>
                <List />
                <Contents />
            </div>
        );
    }
}
export default ItemComponent;

参考

その他

とりあえずいい感じ。気軽さが心地よい。
react / mobx / styledcomponent
次のサービスはいったんこの組み合わせで行く。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7