20190704のReactに関する記事は8件です。

nodejs + mysql + React + ReduxでCRUDアプリを作る Part1

概要

シンプルなCRUD(create, read, update, delete)アプリをデータベースはmysql, フロントエンドはReact + Reduxで作ってみます.
このPart1ではmysqlの設定, node.jsでAPIサーバーの作成まで行います.

mysqlのインストール, 接続

まずmysqlをインストールします.

$ brew update
$ brew install mysql

インストールが終わったら内容を見てみます.

$ brew info mysql

次にデータベースを起動します. 起動はmysql.server start(停止はmysql.server stop)

$ mysql.server start
Starting MySQL
. SUCCESS! 

パスワードを設定しておきます.

$ mysql_secure_installation

聞かれることは基本的にYesで答えます. パスワードは覚えておきましょう.
設定が終わったら接続します.

$ mysql -uroot -p
Enter password:[設定したパスワード]

接続はexitで抜けれます.

mysqlでDB, テーブルの作成

mysqlに接続した状態でまずDBを作成します.

mysql> CREATE DATABASE sample;

DBの作成結果を確認します.

mysql> show databases;

作成したDBを使用可能にします.

mysql> use sample;

次にテーブルを作成します.
idカラムにはauto_incrementを設定しておきます.
nameとstatusにはnot nullを設定します.
nameはuniqueにしておきます.

mysql> create table user (id int auto_increment, name varchar(10) not null unique, status varchar(10) not null,

テーブルの作成結果を確認します.

mysql> show tables;
+------------------+
| Tables_in_sample |
+------------------+
| user             |
+------------------+
1 row in set (0.01 sec)

テーブルの定義を確認する

mysql> desc user;
+--------+-------------+------+-----+---------+----------------+
| Field  | Type        | Null | Key | Default | Extra          |
+--------+-------------+------+-----+---------+----------------+
| id     | int(11)     | NO   | MUL | NULL    | auto_increment |
| name   | varchar(10) | NO   | PRI | NULL    |                |
| status | varchar(10) | NO   |     | NULL    |                |
+--------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

ユーザーを作成します.
nodeがユーザー, localhostがホストになります.
パスワードは後で使うので覚えておきましょう.

mysql> create user 'node'@'localhost' identified with mysql_native_password by 'パスワード';

ユーザーの作成結果を確認します.

mysql> SHOW GRANTS for 'node'@'localhost';

参照可能テーブルを指定します.

mysql> GRANT ALL ON sample.* to node@localhost;

権限の付与結果を確認します.

mysql> SHOW GRANTS for 'node'@'localhost';

試しにサンプルデータを作成してみます.

mysql> insert into user(name, status) values('Katsuomi', 'student');

確認してみます.

mysql> select * from staff;
+----+----------+---------+
| id | name     | status  |
+----+----------+---------+
|  1 | Katsuomi | student |
+----+----------+---------+
1 row in set (0.01 sec)

もう1人作ってみます

mysql> insert into user(name, status) values('Junki', 'student');

確認します.

mysql> select * from user;
+----+----------+---------+
| id | name     | status  |
+----+----------+---------+
|  1 | Katsuomi | student |
|  2 | Junki    | student |
+----+----------+---------+
2 rows in set (0.00 sec)

無事にidがauto_incrementされていることを確認しました.

APIサーバーの作成

適当にディレクトリを作ってserver.jsを作り, そこに書いていきます.

$ mkdir crud-node & cd crud-node
$ npm init -y
$ npm i mysql express body-parser
server.js
const express = require('express');
const mysql = require('mysql');
const bodyParser = require('body-parser');
const cors = require('cors')({origin: true});
const app = express();
app.use(bodyParser.json());
app.use(cors);

const client = mysql.createConnection({
    host: 'localhost',
    user: 'node',
    password: 'HashSignBack1484?_!',
    port : 3306,
    database: 'sample'
});

client.connect(function (err) {
    if (err) {
        console.error('error connecting: ' + err.stack);
        return;
    }
    console.log('connected as id ' + client.threadId);
});

// read 
app.get('/user', (req, res) => {
    client.query('SELECT * from user;', (err, rows, fields) => {
        if (err) throw err;

        res.send(rows);
    });
});

// create 
app.post('/user/create', (req, res) => {
    const name = req.body.name;
    const status = req.body.status;
    client.query('INSERT INTO user SET ?', {name: name, status: status}, (err, result) => {
        if (err) throw err;
        res.send(result);
    })
});

// update 
app.put('/user/update', (req, res) => {
    const id = req.body.id;
    const status = req.body.status;
    client.query('UPDATE user SET status = ? WHERE id = ?', [status, id], (err, result) => {
        if (err) throw err;
        client.query('SELECT * from user;', (err, rows, fields) => {
            if (err) throw err;
            res.send(rows);
        });
    })
});

// delete 
app.delete('/user/delete', (req, res) => {
    const id = req.body.id;
    client.query(`DELETE FROM user WHERE id = ?`, [id], (err, result) => {
        if (err) throw err;
        client.query('SELECT * from user;', (err, rows, fields) => {
            if (err) throw err;
            res.send(rows);
        });
    });
});

app.listen(3001, () => console.log('Listening on port 3001!'))

それではcurlでテストしていきましょう.

作成

$ curl -X POST -H "Content-Type:application/json" http://localhost:3000/user/create  -d '{"name":"taro", "status": "adult"}'

更新

$ curl -X PUT -H "Content-Type:application/json" http://localhost:3000/user/update  -d '{"name":"taro", "status": "student"}'

閲覧

curl http://localhost:3000/user

削除

$ curl -X DELETE -H "Content-Type:application/json" http://localhost:3000/user/delete  -d '{"name":"taro"}'

最後に

次回はReact + Reduxでフロントエンドを作っていきます.

Happy Hacking :sunglasses: !

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

redux-form (5) - Initialize From State

redux-form (1) - Simple Form Example
redux-form (2) - Synchronous Validation Example
redux-form (3) - Field-Level Validation Example
redux-form (4) - Submit Validation Example
redux-form (5) - Initialize From State


ReactでForm componentを作るときに、とても便利なredux-formの説明です。

redux-formの概説についてはまず以下の記事を参考にしてください。

redux-form (1) - Simple Form Example

Initialize From State

Initialize From State - Getting Started With redux-form

redux-formでは、decorated form component(wrapped component)のform stateを初期化するために、initialValuesを用います。initialValuesは、例えばサーバから取得した値を、React Redux の connect()を使って、mapStateToPropsでRedux state をinitialValues propに変換して、decorated form componentに渡すようにします。

initialValuesでformを初期化できるのは、デフォルトで1回のみです。

decorated form componentはreset() propを受け取りますが、reset()を実行することでformをinitialValuesに戻すことができます。

以下が、reducerとaction creatorのソースです。

src/account.js
// Quack! This is a duck. https://github.com/erikras/ducks-modular-redux
const LOAD = 'redux-form-examples/account/LOAD'

const reducer = (state = {}, action) => {
  switch (action.type) {
    case LOAD:
      return {
        data: action.data
      }
    default:
      return state
  }
}

/**
 * Simulates data loaded into this reducer from somewhere
 */
export const load = data => ({ type: LOAD, data })

export default reducer

以下がredux-formのサンプルです。ポイントの説明を下にまとめてあります。

src/InitializeFromStateForm.js
import React from 'react'
import { connect } from 'react-redux'
import { Field, reduxForm } from 'redux-form'
import { load as loadAccount } from './account'

const data = {
  // used to populate "account" reducer when "Load" is clicked
  firstName: 'Jane',
  lastName: 'Doe',
  age: '42',
  anniversaryDate: '2018-08-22',
  sex: 'female',
  employed: true,
  favoriteColor: 'Blue',
  bio: 'Born to write amazing Redux code.'
}

const colors = ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet']

let InitializeFromStateForm = props => {
  const { handleSubmit, load, pristine, reset, submitting } = props
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <button type="button" onClick={() => load(data)}>
          Load Account
        </button>
      </div>
      <div>
        <label>First Name</label>
        <div>
          <Field
            name="firstName"
            component="input"
            type="text"
            placeholder="First Name"
          />
        </div>
      </div>
      <div>
        <label>Last Name</label>
        <div>
          <Field
            name="lastName"
            component="input"
            type="text"
            placeholder="Last Name"
          />
        </div>
      </div>
      <div>
        <label>Age</label>
        <div>
          <Field name="age" component="input" type="number" placeholder="Age" />
        </div>
      </div>
      <div>
        <label>Anniversary Date</label>
        <div>
          <Field name="anniversaryDate" component="input" type="date" />
        </div>
      </div>
      <div>
        <label>Sex</label>
        <div>
          <label>
            <Field name="sex" component="input" type="radio" value="male" />{' '}
            Male
          </label>
          <label>
            <Field name="sex" component="input" type="radio" value="female" />{' '}
            Female
          </label>
        </div>
      </div>
      <div>
        <label>Favorite Color</label>
        <div>
          <Field name="favoriteColor" component="select">
            <option value="">Select a color...</option>
            {colors.map(colorOption => (
              <option value={colorOption} key={colorOption}>
                {colorOption}
              </option>
            ))}
          </Field>
        </div>
      </div>
      <div>
        <label htmlFor="employed">Employed</label>
        <div>
          <Field
            name="employed"
            id="employed"
            component="input"
            type="checkbox"
          />
        </div>
      </div>
      <div>
        <label>Bio</label>
        <div>
          <Field name="bio" component="textarea" />
        </div>
      </div>
      <div>
        <button type="submit" disabled={pristine || submitting}>
          Submit
        </button>
        <button type="button" disabled={pristine || submitting} onClick={reset}>
          Undo Changes
        </button>
      </div>
    </form>
  )
}

// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
InitializeFromStateForm = reduxForm({
  form: 'initializeFromState' // a unique identifier for this form
})(InitializeFromStateForm)

// You have to connect() to any reducers that you wish to connect to yourself
InitializeFromStateForm = connect(
  state => ({
    initialValues: state.account.data // pull initial values from account reducer
  }),
  { load: loadAccount } // bind account loading action creator
)(InitializeFromStateForm)

export default InitializeFromStateForm

React-Reduxのconnect()関数について少し説明します。

mapStateToPropsinitialValues propが設定されています。initialValues は reduxForm が処理します。initialValues は { field1: 'value1', field2: 'value2' } の形をしており、componentWillMount() において form を初期化します。
initialValuesはここの例のようにpropで受け渡す以外に、reduxForm() のconfig parameterで受け渡す方法もあります。

mapDispatchToPropsobject を指定することができます。この場合、objectの各fieldがaction creatorとなり、 React-Redux は dispatchをaction creatorにbindします。
connect() -React Redux

InitializeFromStateForm = connect(
  state => ({          // mapStateToProps
    initialValues: state.account.data 
  }),
  { load: loadAccount } // mapDispatchToProps
)(InitializeFromStateForm)

以下はindex.jsですが、オリジナルのものを最小化してあります。

src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import { reducer as reduxFormReducer } from 'redux-form'
import account from './account'

const dest = document.getElementById('content')

const reducer = combineReducers({
  account,
  form: reduxFormReducer // mounted under "form"
})
const store = createStore(reducer)

const showResults = values =>
  new Promise(resolve => {
    setTimeout(() => {
      // simulate server latency
      window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`)
      resolve()
    }, 500)
  })

let render = () => {
  const InitializeFromStateForm = require('./InitializeFromStateForm').default
  ReactDOM.hydrate(
    <Provider store={store}>
      <h2>Form</h2>
      <InitializeFromStateForm onSubmit={showResults} />
    </Provider>,
    dest
  )
}

render()

実行画面

初期画面です

image.png

「Load Account」ボタンでformを初期化します。最初の一回目だけ有効です。初期状態を変更してから、再度初期状態に戻すためには、UndoChangesボタンでreset()を発行する必要があります。

image.png

submitボタンを押します。

image.png

今回は以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VS Codeで開発環境を整える

参考URL:https://qiita.com/gumiTECH/items/4b82e4e3757f226df6b0

・node jsをインストールしておき npmコマンドが使えるようにしておく。
・VS Codeをインストールしておく。

1. Reactの雛形アプリケーションを作成

以下のコマンドでreact関連のパッケージをインストールする。

npm install -g create-react-app

その後、任意のフォルダに移動し、以下のコマンドを実行(以下は、[react-sample]というアプリ名の例) ※そこそこ時間かかる。

create-react-app react-sample
以下のコマンドを実行する。
cd react-sample
npm start

http://localhost:3000/
でアプリケーションのページを開くことができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+ReduxにRedux-Sagaを導入して非同期処理(axios)をさせる手順

ソースコード
https://github.com/tontoko/react-redux-saga-rails

↓こちらの記事を参考にまずReact+Rails(API)の環境を整えました。

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

非常に分かりやすいです。

Reduxの導入

sudo npm install redux react-redux --save

ActionCreator

crud-front/src/Actions/actions.js
export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
}

Reducer

crud-front/src/Reducers/productsReducer.js
// 中味はまだ空

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
            })
        case 'UPDATE':
            return Object.assign({}, state, {
            })
        case 'DELETE':
            return Object.assign({}, state, {
            })
        case 'INIT':    
            return Object.assign({}, state, {
            })
        default:
            return state
    }
}
crud-front/src/Reducers/reducers.js
// Reducer達を一つに纏める
// 今回は必要ないけど。。

import {combineReducers} from 'redux'
import productsReducer from './productsReducer'

export default combineReducers({
    products: productsReducer,

});

index.js

crud-front/src/index.js
// -- 省略 --
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import Reducers from './Reducers/reducers'

const store = createStore(
    Reducers,
)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

MainContainer.js

crud-front/src/Components/MainContainer.js
// -- 省略 --
import { connect } from 'react-redux'
import Actions from '../Actions/actions'
import { bindActionCreators } from 'redux'

const mapStateToProps = state => {
    return state
}

const mapDispatchToProps = dispatch => {
    return {
        init: () => dispatch(Actions.init()),
        create: (data) => dispatch(Actions.create(data)),
        update: (id, product) => dispatch(Actions.update(id, product)),
        delete: (id) => dispatch(Actions.delete(id)),
    }
}

//// こっちでもいいかな
// const mapDispatchToProps = dispatch => {
//     return bindActionCreators(Actions, dispatch)
// }

export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)

非同期処理をどこに書くべきか

Redux導入の雛形はできたものの、ここで問題が起きる。

reduxで非同期処理をするいくつかの方法(redux-thunk、redux-saga)
https://qiita.com/muiscript/items/63386fd65c7e9f06f5d4

Actionをプレーンに保てテストもしやすい、というRedux-Sagaを試してみます。

Redux-Sagaの導入

Redux-Sagaの概要については以下の記事もわかりやすいです。

redux-sagaで非同期処理と戦う
https://qiita.com/kuy/items/716affc808ebb3e1e8ac

【React】 redux-saga でAPIを叩く
https://k-tomoo.hatenablog.com/entry/2018/03/12/151045

まずはインストールします。

sudo npm install redux-saga --save

Saga部分をInit処理を例にとって書いていきます。

crud-front/src/Saga/Init.js
import axios from "axios"
import { put, call, takeEvery } from 'redux-saga/effects';

const initAjax = () => axios.get('http://localhost:3001/products')
    .then((res) => {
        const data = res.data
        console.log(data)
        return { data }
    })
    .catch((error) => {
        return { error }
    })

function* initProduct() {
    // 3.
    const { data, error } = yield call(initAjax);
    console.log(data)

    if (data) {
        // 4.
        yield put({ type: "INIT_SUCCEEDED", data });
    } else {
        // todo: エラーハンドリング
        // 今回はエラー処理は省きます
    }
}

// 1.& 2.
export default [takeEvery("INIT", initProduct)];

パッとみてもよくわからないと思うので順に見ていきます。

  1. takeEveryの第一引数で指定されたアクションがどこかで呼ばれる

  2. そのアクションの完了を待って第二引数が呼ばれる

  3. yield call()で中の関数が実行され、Promiseオブジェクトが帰ってくるまで待つ

  4. yield putで新たにActionをdispatchする

ざっくりこのような流れで非同期処理を実現しています。

function* などを見て戸惑った方は以下の記事がわかりやすいです。
https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://qiita.com/kura07/items/d1a57ea64ef5c3de8528

Redux-Sagaを組み込む

crud-front/src/index.js
import createSagaMiddleware from 'redux-saga'
import { all } from 'redux-saga/effects'

import Init from './Saga/Init'
import Create from './Saga/Create'
import Update from './Saga/Update'
import Delete from './Saga/Delete'

// ここで一つにまとめます
function* rootSaga() {
    yield all([
        ...Init,
        ...Create,
        ...Update,
        ...Delete,
    ])
}

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
    Reducers,
    applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(rootSaga)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

呼び出し用、成功した処理用、エラー処理用と分けてやる必要があります。(エラー分は今回は省略)

crud-front/src/Actions/actions.js
export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    createSuccess: (data) => {
        return { type: 'CREATE_SUCCEEDED', data }
    },
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    updateSuccess: (id, data) => {
        return { type: 'UPDATE_SUCCEEDED', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    deleteSuccess: (id) => {
        return { type: 'DELETE_SUCCEEDED', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
    initSuccess: (data) => {
        return { type: "INIT_SUCCEEDED", data}
    },
}
crud-front/src/Reducers/productsReducer.js
import axios from "axios"

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'CREATE_SUCCEEDED':
            return Object.assign({}, state, {
                products: [...state.products, action.data],
                isFetching: false,
            })
        case 'UPDATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'UPDATE_SUCCEEDED':
            const updateIndex = state.products.findIndex(x => x.id === action.id)
            const updatedProductsState = state.products
            updatedProductsState.splice(updateIndex, 1, action.data)
            return Object.assign({}, state, {
                products: updatedProductsState,
                isFetching: false,
            })
        case 'DELETE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'DELETE_SUCCEEDED':
            const deleteIndex = state.products.findIndex(x => x.id === action.id)
            const deletedProductsState = state.products
            deletedProductsState.splice(deleteIndex, 1)
            return Object.assign({}, state, {
                products: deletedProductsState,
                isFetching: false,
            })
        case 'INIT':    
            return Object.assign({}, state, {
                products: [],
                isFetching: true,
            })
        case 'INIT_SUCCEEDED':
            return Object.assign({}, state, {
                products: action.data,
                isFetching: false,
            })
        default:
            return state
    }
}

ここまでで導入は完了です。
後は例としてinit()を呼んでやります。

crud-front/src/Components/MainContainer.js
class MainContainer extends React.Component {
// -- いろいろ省略 --
    componentDidMount() {
        this.props.init()
    }

    render() {
        if (this.props.isFetching === true) {
            return (<div />)
        } else {
            return (
                <div className='app-main'>
                    <FormContainer createProduct={this.props.create} />
                    <ProductsContainer deleteProduct={this.props.delete} updateProduct={this.props.update} />
                </div>
            );
        }
    }
}
// -- いろいろ省略 --
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】最小限のreact-router実装で仕組みを理解する

概要

react-routerの使い方を理解するために最小限の実装をする

成果物

out.gif

手順

  1. Reactの新規プロジェクト作成
  2. react-routerをインストール
  3. srcディレクトリの中身を削除
  4. index.jsとApp.jsを作成

1. Reactの新規プロジェクト作成

$ npx create-react-app hogehoge

2. react-routerをインストール

$ cd hogehoge
$ npm install react-router

3. srcディレクトリの中身を削除

$ rm -rf ./src/*

4. index.jsとApp.jsを作成

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './App.js';

ReactDOM.render(
    <AppRouter />,
    document.getElementById('root')
);
App.js
import React from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';

function Index(){
    return <h2>Home page is here!</h2>
}

function About(){
    return <h2>This is About page.</h2>
}

function Users(){
    return <h2>This is User page.</h2>
}

function AppRouter(){
    return(
        <Router>
            <div>
                <nav>
                    <ul>
                        <li>
                            <Link to='/'>Home</Link>
                        </li>
                        <li>
                            <Link to='/about/'>About</Link>
                        </li>
                        <li>
                            <Link to='/users/'>Users</Link>
                        </li>
                    </ul>
                </nav>
                <Route path='/' exact component={Index} />
                <Route path='/about/' component={About} />
                <Route path='/users/' component={Users} />
            </div>
        </Router>
    );
}

export default AppRouter;

Happy Hacking :sunglasses: !

参考

https://reacttraining.com/react-router/web/guides/quick-start

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React HooksのみでWebアプリを作りました! ( 参考にどうぞ ! )

この記事について

私が作ったWebアプリをオープンソース化したので、そのWebアプリがどのような形で作られているかを解説した記事です。

オープンソース化した理由としては、これからReact Hooksを導入する人Webアプリを作ってみたい人などの参考になればと思いオープンソース化しました。

作ったもの

画像投稿ができるWebアプリです。

デモページ

デモページ

ソースコード

製作期間

  • 三か月ほど

実装した機能

  • 認証機能
  • 画像投稿機能
  • いいね機能
  • フォロー機能
  • 通知機能

使用したものなど

  • React
  • React-Router
  • dayjs
  • TypeScript
  • Firebase Authentication
  • Firebase FireStore
  • Firebase Storage
  • Firebase Hosting

なぜ作ったのか?

前回の記事でJavaScriptで使えるフレームワークを作成したのですが、記事を投稿した後、中々上手い実装などが浮かばず、行き詰ってしまいました。
この状況から何とか抜け出そうと考えた結果、Webアプリを作ってみようと思い、出来たのが今回のWebアプリです。

解説

ここからは、Webアプリがどのように出来ているかを解説したいと思います。

フォルダー構成について

フォルダー構成についてあまり深い関心が無い方もいるかもしれませんが、フォルダー構成はとても大切です。
個人で開発する場合も、チームで開発する場合も、フォルダー構成がしっかりしてないと良い開発はできないと私は思っています。
しかし、世の中の記事を見ていてもフォルダー構成に関する記事などがあまりありませんでした。
本当はあるのかもしれませんが、私には少ないように思えました。
なので、私が考えたフォルダー構成を皆さんに伝えたいと思います。

│
├─node_modules
├─public
└─src
  ├─assets
  | └─icons
  ├─components
  │  ├─atoms
  │  ├─modules
  │  ├─molecules
  │  ├─organisms
  │  ├─pages
  │  └─templates
  ├─logics
  │  ├─actions
  │  ├─reducer
  │  └─util
  |    └─uses
  └─tests

フォルダーのみ表示してます

解説は、srcフォルダー内のみにします。

src/assets

画像などを保存するフォルダーです。

src/components

Reactコンポーネントを記述したファイルを入れるフォルダーです。
src/componentsのフォルダー構成は、Atomic Designに基づいて以下のようにコンポーネントのファイルを分割してます。

components/atoms

一番小さいコンポーネントの単位です。
React Hooksを使って無いコンポーネントのみを入れるようにします。

atomsのコンポーネント例
atomsのコンポーネント例
import React from "react";
import styles from "./LoadingBar.module.scss";

export const LoadingBar: React.FC<{ isLoading: boolean }> = ({ isLoading }) => {
  return isLoading ? <div className={styles.loading_bar} /> : null;
};


components/molecules

atomsの次に小さいコンポーネントの単位です。
こちらも、atomsと同じくReact Hooksを使ってないコンポーネントのみを入れますが、
atomsとの違いは、要素数がatomsより多いコンポーネントになります。( 要素数がだいたい3~4個以上 )

moleculesのコンポーネント例
moleculesのコンポーネント例
import React, { FormEvent } from "react";
import styles from "./SearchInput.module.scss";

interface SearchInputProps extends React.Props<{}> {
  maxWidth?: number;
  placeholder?: string;
  onSubmit: (inputValue: string) => void;
}

export const SearchInput: React.FC<SearchInputProps> = ({
  onSubmit,
  maxWidth,
  placeholder = "キーワードを検索"
}) => {
  const submit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    onSubmit((e.currentTarget.search as HTMLInputElement).value);
  };

  return (
    <form
      className={styles.search__input}
      style={{ maxWidth }}
      onSubmit={submit}
    >
      <div className={styles.search_icon} />

      <input
        type="text"
        name="search"
        autoComplete="off"
        placeholder={placeholder}
      />
    </form>
  );
};


components/organisms

コンポーネントの大きさ関係なくReact Hooksを使っていれば問答無用でこちらのフォルダーに入れられます。

organismsのコンポーネント例
organismsのコンポーネント例
import React from "react";
import { useUsers } from "../../../logics/util/uses/users";
import { useUtil } from "../../../logics/util/uses/util";
import { User } from "../../../types";
import { Button } from "../../atoms/Button";

interface FollowButtonProps extends React.Props<{}> {
  uid: User["id"];
  className?: string;
}

export const FollowButton: React.FC<FollowButtonProps> = React.memo(
  ({ uid, className }) => {
    const { alerts } = useUtil();
    const { isFollowee, current_user, followUser, unfollowUser } = useUsers();
    const isFollowed = isFollowee(uid);
    const currentUserId = current_user ? current_user.id : "";
    const onClick = () => {
      if (uid && currentUserId && currentUserId !== uid) {
        if (isFollowed) {
          unfollowUser(uid);
        } else {
          followUser(uid);
        }
      } else if (currentUserId) {
        alerts.follow_user.warn();
      } else {
        alerts.required_login.warn();
      }
    };

    return (
      <Button
        disabled={!uid}
        className={className}
        color={isFollowed ? "red" : "blue"}
        size="small"
        onClick={onClick}
      >
        {isFollowed ? "フォローを解除" : "フォローする"}
      </Button>
    );
  }
);


components/pages

一つのページを構成するコンポーネントを入れるフォルダーです。

pagesのコンポーネント例
pagesのコンポーネント例
import React from "react";
import { RouteComponentProps } from "react-router";
import { Board } from "../../atoms/Board";
import { BoardItem, NotifyBoardItem } from "../../atoms/BoardItem";
import { DashboardTemplate } from "../../templates/DashboardTemplate";
import styles from "./Dashboard.module.scss";
import { useDashboard } from "./use";

export const Dashboard: React.FC<RouteComponentProps> = () => {
  const { notifies, finished, getNotifies } = useDashboard();

  return (
    <DashboardTemplate>
      <main className={styles.dashboard}>
        <Board headerLabel="通知一覧" minWidth={500}>
          {notifies.map(notify => (
            <NotifyBoardItem key={`notify-item-${notify.id}`} notify={notify} />
          ))}

          {finished ? null : (
            <BoardItem
              className={styles.link}
              label="さらに10件の通知を表示"
              onClick={getNotifies}
            />
          )}
        </Board>
      </main>
    </DashboardTemplate>
  );
};


components/templates

pagesでの共通部分を抜き出して、再利用できるようにしたコンポーネントを入れるフォルダーです。

templatesのコンポーネント例
templatesのコンポーネント例
import React, { useEffect } from "react";
import useReactRouter from "use-react-router";
import { GlobalHeader } from "../../organisms/GlobalHeader";
import styles from "./PageTemplate.module.scss";

interface PageTemplateProps extends React.Props<{}> {
  title?: string;
  background?: string;
  className?: string;
}

export const PageTemplate: React.FC<PageTemplateProps> = ({
  children,
  className,
  background
}) => {
  const { location } = useReactRouter();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location.pathname]);

  return (
    <div className={styles.page} style={{ background }}>
      <GlobalHeader />

      <div className={`${styles.body} ${className || ""}`}>{children}</div>

      <div className={styles.footer}>
        <p>©︎ 2019 uttk</p>
      </div>
    </div>
  );
};


components/modules

上記のコンポーネントの分割に当てはまらないコンポーネントを入れるフォルダーです。

modulesのコンポーネント例
modulesのコンポーネント例
import React, { useEffect, useState } from "react";
import { IconButton } from "../../atoms/Button";
import styles from "./Alerts.module.scss";

/**
 * 長いので省略
 */

interface AlertProps extends React.Props<{}> {}

export const Alert: React.FC<AlertProps> = React.memo(() => {
  const [alerts, setAlert] = useState<AlertElement[]>([]);
  const len = alerts.length;

  AlertsMg.elements = alerts;
  AlertsMg.dispatch = (elements: AlertElement[]) => {
    setAlert(elements);
  };

  return (
    <div>
      {alerts.map((alert, i) => (
        <AlertBar
          key={alert.id}
          top={(len - i) * 64}
          alert={alert}
          displayTime={3000}
        />
      ))}
    </div>
  );
});

interface AlertBarProps extends React.Props<{}> {
  top: number;
  alert: AlertElement;
  displayTime: number;
}

export const AlertBar: React.FC<AlertBarProps> = React.memo(
  ({ top, alert, displayTime }) => {
    const [style, setStyle] = useState(alert.style);

    useEffect(() => {
      let timeoutId = setTimeout(() => {
        setStyle(`${style} ${styles.alert_show}`);

        timeoutId = setTimeout(() => {
          setStyle(alert.style);
          timeoutId = setTimeout(alert.onClose, 300);
        }, displayTime);
      }, 0);

      return () => clearTimeout(timeoutId);
    }, []);

    return (
      <li className={style} style={{ top }}>
        <p>{alert.message}</p>
        <IconButton
          size={16}
          icon="close_white"
          color="transparent"
          className={styles.close_btn}
          onClick={alert.onClose}
        />
      </li>
    );
  }
);


src/logics

ロジック部分の処理を書いたファイルを格納するフォルダーです。

logics/actions

通信処理などをする関数を定義したファイルを入れるフォルダーです。

logics/reducer

後述するuseClutchで使うreducerを定義したファイルを入れるフォルダーです。

logics/util

actionsreducerには当てはまらないものをここに入れます。

logics/util/uses

このフォルダーには、コンポーネントで使うReact HooksカスタムHooksを定義したファイルを入れます。

コンポーネントについて

コンポーネントは、以下のフォルダー構成になります。

コンポーネントのフォルダー構成
/ComponentName
├─ index.tsx                  <- JSXを書くファイル
├─ ComponentName.moduel.scss  <- index.tsxのスタイルを書くファイル (フォルダーと同じ名前にする)
└─ use.ts                     <- index.tsxで使うコールバックや変数を定義するファイル

index.tsxに描画部分を書き、ロジックなどはuse.tsに書くことによって、
描画部分はより描画に専念して書けますし、ロジックも同じように書けます。
修正する時も、色々とやりやすかったりします。
今回のWebアプリではpagesのみこのようにしていますが、後々は全部のコンポーネントをこの構成したいです。

ComponentName.module.scssは、index.tsx

index.tsx
import styles from "./ComponentName.module.scss"

のようにするためです。

State管理について

このWebアプリでは、Reactは使っていますが、Redux使って無いので工夫する必要があります。

useClutch

このWebアプリでは、useClutchというカスタムHooksを作成しました。
以下が、データフローになります。

data-flow.png

useClutchがやっていることは単純で、Promiseを返すreducerを実行して、awaitして結果を受け取ると、
内部でsetStateをして描画を更新します。
こうすることで、reducer内で非同期処理が出来て処理を一か所にまとめることができます。

clutchの使用例
type Action = { type: "increment" } | { type: "decrement" };

interface StoreType {
  counter: number;
}

const state : StoreType  = {
  counter: 0
};

const sleep = (t:number) => new Promise(r => setTimeout(r,t,t));

const reducer = async (state:number, action:Action) : Promise<StoreType> => {
  switch(action.type){
    case "increment":
      await sleep(5000);
      return state + 1;

    case "decrement":
      await sleep(5000);
      return state - 1;

    default:
      return state;
  }
}

const App : React.FC = () => {
  const clutch = useClutch(reducer, store);
  const increment = () => clutch.dispatch("increment", { type: "increment" }).catch(console.error);
  const decrement = () => clutch.dispatch("decrement", { type: "decrement" }).catch(console.error);

  // 複数のactionを繋げることもできます。
 // 描画更新は、すべてのactionが終了したときに発生します。
  const add = () => clutch.pipe(
    "test",
    state => ({ type: "increment" }),
    state => state.counter > 10 ? null : ({ type: "increment" }) // 前のactionの結果を受け取って10以下ならさらにincrementする
  ).catch(console.error)

  return (
    <div>
      <p>カウント : {clutch.counter}<p>
      <button onClick={increment}>カウントアップ</button>
      <button onClick={decrement}>カウントアップ</button>
      <button onClick={add}>1足して10以下ならさらに1足す</button>
    </div>
  );
}

デザインについて

デザインは勉強の意味も込めて自分でやりましたが、ダサいので参考にはならないと思います。

しかしあれですね、デザイナーさんは凄いですね!
私もデザインについてもっと勉強しないといけないと感じました。( 特にAdobe。。。 )

React HooksとTypeScriptは相性がいい!

React HooksTypeScriptを使って開発しましたが、これが予想以上に良かったです!
React Hooksで、コンポーネントの記述量を減らして、その減らした分をTypeScriptに使うことで、
従来の記述量と同じくらいか少ないのに、型安全で、より設計しやすいコーディングをすることができました。
これから開発する際には、TypeScriptを使ったほうがよさそうですね。

また、React Hooksは柔軟に組み合わせることができるので、複雑な処理も関数に閉じ込めることができます。
上記で紹介したuseClutchカスタムHooksで実装してます。

useClutchのソースコード
useClutch.ts
import { useRef, useState } from "react";

type Reducer<S, A> = (preState: S, action: A) => Promise<S>;

type GetStoreType<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
  ? S
  : any;

type GetActionType<R extends Reducer<any, any>> = R extends Reducer<
  any,
  infer A
>
  ? A
  : any;

type ActionCreator<S, A> = (preState: S) => A | null;

type RequestStatus = "start" | "success" | "cancel" | "error";

type ListenRequestCallback = (request: string, status: RequestStatus) => void;

type CancelCallback = () => void;

export interface Clutch<StoreType, ActionType> {
  state: StoreType;
  request: <T>(
    req: string,
    promiseCreator: () => Promise<T>
  ) => Promise<T | null>;
  cancelRequest: (request: string) => boolean;
  listenRequest: (cb: ListenRequestCallback) => () => void;
  isLoading: (request?: string) => boolean;
  pipe: (
    request: string,
    ...funcs: Array<ActionCreator<StoreType, ActionType>>
  ) => Promise<void>;
  dispatch: (request: string, action: ActionType) => Promise<void>;
}

export const useClutch = <R extends Reducer<any, any>>(
  asyncReducer: R,
  initializeState: GetStoreType<R>
): Clutch<GetStoreType<R>, GetActionType<R>> => {
  type StoreType = GetStoreType<R>;
  type ActionType = GetActionType<R>;

  // 描画を更新するためのState
  const [pureState, setState] = useState<StoreType>(initializeState);

  const {
    progressCancel, 
    progressPromise, 
    listenCallbacks,
    notifyRequest,
    resolveAsync,
    updateState, 
    clutch
  } = useRef({
    // 実行中のPromise処理をキャンセルするコールバックを保持するMap
    progressCancel: new Map<string, CancelCallback>(),

    // 実行中のPromiseインスタンスを保持するMap
    progressPromise: new Map<string, Promise<any>>(),

    // 非同期処理状況をlistenする関数を保持するSet
    listenCallbacks: new Set<ListenRequestCallback>(),

    // 非同期処理の状態をlistenCallbackに伝える関数
    notifyRequest: (request: string, status: RequestStatus) => {
      listenCallbacks.forEach(cb => cb(request, status));
    },

    // 引数に渡されたPromiseを実行しawaitして結果を返す
    resolveAsync: <T>(
      request: string,
      cb: () => Promise<T>
    ): Promise<T | null> => {
      const promise = progressPromise.get(request);

      if (promise) {
        return promise as Promise<T | null>;
      }

      notifyRequest(request, "start");

      const cancelCallback = new Promise<"cancel">(re => {
        progressCancel.set(request, () => re("cancel"));
      });

      const progress = Promise.race([cb(), cancelCallback])
        .then(result => {
          notifyRequest(request, result === "cancel" ? "cancel" : "success");

          progressCancel.delete(request);
          progressPromise.delete(request);

          return result === "cancel" ? null : result;
        })
        .catch(e => {
          notifyRequest(request, "error");

          progressCancel.delete(request);
          progressPromise.delete(request);

          return Promise.reject(e);
        });

      progressPromise.set(request, progress);

      return progress;
    },

    // stateを更新する関数
    updateState: async (
      request: string,
      oldState: StoreType,
      promiseCreator: (oldState: StoreType) => Promise<StoreType>
    ) => {
      const newState = await resolveAsync(request, () =>
        promiseCreator(oldState)
      );
      const updated: Partial<StoreType> = {};

      if (newState) {
        for (const key in oldState) {
          if (oldState[key] !== newState[key]) {
            updated[key] = newState[key];
          }
        }

        if (Object.keys(updated).length > 0) {
          clutch.state = { ...clutch.state, ...updated };
          setState(clutch.state);
        }
      }
    },

    // Propsに渡されるclutchオブジェクト
    clutch: {
      state: pureState,

      // 処理が実行中かのフラグを返す関数
      isLoading: (request?: string): boolean => {
        if (request) {
          return progressPromise.has(request);
        }

        return !!progressPromise.size;
      },

      // reducerを通さないPromiseを監視するようにする
      request: <T>(
        req: string,
        promiseCreator: () => Promise<T>
      ): Promise<T | null> => {
        return resolveAsync<T>(req, promiseCreator);
      },

      // 実行中のPromiseを中断する
      cancelRequest: (request: string): boolean => {
        const cancel = progressCancel.get(request);

        if (cancel) {
          cancel();
          return true;
        } else {
          return false;
        }
      },

      // listenCallbackを設定する関数
      listenRequest: (cb: ListenRequestCallback): (() => void) => {
        listenCallbacks.add(cb);
        return () => listenCallbacks.delete(cb);
      },

      // 引数に渡されたActionCreatorの順番で、reducerを実行する
      pipe: async (
        request: string,
        ...funcs: Array<ActionCreator<StoreType, ActionType>>
      ): Promise<void> => {
        const promiseCreator = async (oldState: StoreType) => {
          for (const fn of funcs) {
            const action = fn(oldState);

            if (action) {
              oldState = await asyncReducer(oldState, action);
            }
          }

          return oldState;
        };

        await updateState(request, { ...clutch.state }, promiseCreator);
      },

      // action&payloadをreducerに渡して実行する
      dispatch: async (request: string, action: ActionType): Promise<void> => {
        const promiseCreator = (oldState: StoreType) => {
          return asyncReducer(oldState, action);
        };

        await updateState(request, { ...clutch.state }, promiseCreator);
      }
    }
  }).current;

  return clutch;
};


使った技術の感想

React & React Hooks

Reactは以前から使っていたので色々とやり易かったですが、React Hooksによってより使いやすくなりました。
例えば、上で説明したpagesのコンポーネントのindex.tsxの中身は、以下のようになりました。

pages/Home/index.tsx
import React from "react";
import { RouteComponentProps } from "react-router";
import { Button } from "../../atoms/Button";
import { Icon } from "../../atoms/Icon";
import { PageTemplate } from "../../templates/PageTemplate";
import styles from "./Home.module.scss";
import { useHome } from "./use";

export const Home: React.FC<RouteComponentProps> = () => {
  const { isLogin, isCheked, login, goToTrend } = useHome();

  return (
    <PageTemplate className={styles.page}>
      <div className={styles.home}>
        <div className={styles.title}>
          <div className={styles.wrapper}>
            <h1 className={styles.title_label}>NIJINOWA</h1>

            <div className={styles.wrapper}>
              <p>
                <big>NIJINOWA</big> は二次創作を投稿できるサービスです
              </p>
            </div>

            <div className={styles.container}>
              <div className={styles.wrapper}>
                <Button color="blue" onClick={goToTrend}>
                  トレンドを見る
                </Button>
              </div>

              <div className={styles.wrapper}>
                <Button disabled={!isCheked} onClick={login}>
                  {isLogin
                    ? "ダッシュボードに移動"
                    : "Googleアカウントでログイン"}
                </Button>
              </div>

              <div className={styles.wrapper}>
                <a
                  href="https://twitter.com/uttk8128"
                  target="_blank"
                  className={styles.twitter_btn}
                >
                  <Icon icon="twitter_white" size={32} />
                  製作者のTwitter
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </PageTemplate>
  );
};

見てわかるように、ロジックの処理が書かれていないことが分かります。
これは、useHome()というカスタムHooksにロジック部分を書いているためです。
以下がuseHome()の実際のソースコードになります

pages/Home/use.ts
import { useEffect, useState } from "react";
import useReactRouter from "use-react-router";
import { useAuth } from "../../../logics/util/uses/auth";

export const useHome = () => {
  const { history } = useReactRouter();
  const { isLogin, isCheked, loginCheck, loginWithGoogle } = useAuth();
  const [isPush, setPush] = useState(false);
  const login = () => {
    if (isLogin) {
      history.push("/dashboard");
    } else {
      setPush(true);
      loginWithGoogle();
    }
  };

  useEffect(() => {
    if (!isCheked) {
      loginCheck();
    } else if (isLogin && isPush) {
      history.push("/dashboard");
    }
  }, [history, isCheked, isPush, isLogin]);

  return {
    isLogin,
    isCheked,
    login,
    goToTrend: () => history.push("/trends")
  };
};

こうやって、ViewLogicを分けて書けるようになったのも、React Hooksによるところが大きいかと思います。
また、ファイルを分けているので関心の分離といった観点からもいいのではないでしょうか。

TypeScript

TypeScriptを使うことによって、コンポーネントやカスタムHooksに型情報を追加し、より扱い易いものを作ることができます。
また、上記でも言ったように、本当にReact HooksTypeScriptの相性は良く、
TypeScriptの記述量の多さに嫌気が差す方もいるかとは思いますが、React Hooksがコンポーネントの記述量を減らしてくれているので、
実質今までの記述量とあまり変わらないです。むしろ少なくなっている方です。
さらに、同じ記述量でもTypeScriptの方が型安全ですし、VSCodeなどのTypeScriptをサポートしている
エディタを使えば入力補完も効いて、素晴らしい開発者体験が得られます。
React Hooksを導入する・しているなら、TypeScriptも導入することをお勧めします。

Firebase

Firebaseは、小さいサービスなどを作るのに本当に便利です。
今回のWebアプリでは、Auth,FireStore,Storage,Hostingを使いました。
今回の開発では、特にFireStoreのデータ構造とreducerによる処理の分割が、相性がとてもよかったです。

例えば、userドキュメントfollowerサブコレクションがあったとき、followerreducerを作れば、
それだけで、followerの取得や変更などがアプリに追加できます。
これは、後からデータを追加する際にも同じような感じでアプリを拡張できます。

つまり何が言いたいのかというと、FireStoreコレクションごとにreducerを作ることにより、
コレクションを新しく追加したとき、新しくreducerを作って、Webアプリに機能を追加し、
編集の際も、そのreducerのみを変更すればいいのです。

また、ログイン機能を簡単に入れれたり、サイトを簡単にデプロイ出来たりとFirebaseの凄さに恐縮してしまいます。

作ってみた感想

実は今回Webアプリ製作はフレームワークの行き詰まりの解消のためでもあったので、
普通に作るのではなく、二つの制限をかけて作りました。

制限
1. React.jsで作るが、Classコンポーネントは使わない
2. なるべく、他のnpmモジュールに頼らず自分で作る

上記の制限の中で作ることにより、より設計やデータの流れについて考えるようになり色々とフレームワークのアイディアが見えてきたので、Webアプリを作ってよかったと思いました。

あと、やっぱりモノづくりは楽しい!

後書き

今回のWebアプリを作るきっかけになった、前回の記事で紹介したフレームワークですが、
前回の記事いいね!などしてくれた方たちには大変申し訳ないですが、
このまま開発してもよくならないように感じたので、開発するのは終わりにしたいと思います。
しかし、また別のフレームワークを企画しています!

最後まで、読んでいただきありがとうございます。
何か気になることなどがあれば、お気軽にコメントください!
あと、最近Twitter始めたのでそちらでも大丈夫です!
それではまた?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-loopsでReactのリスト表示を美しく簡潔に書く

2019年も半分を過ぎました。早い、、
今回はReactのリスト表示の話で、react-loopsという新しいライブラリを見つけて使ってみたのでゆるく紹介します。

https://github.com/leebyron/react-loops

作者は元Facebookのエンジニアで、ReactやGraphQL周りのOSSコントリビューターであるLee Byronです。

普通のやり方

JSXのメリットの一つは全てがJavaScriptである点です。リスト表示をする場合は下記のようにArray.mapを使います。

function List() {
  const itemList = ['a', 'b', 'c']

  return (
    <ul>
      {itemList.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

react-loopsを使うと

react-loopsを使うと、少しテンプレートっぽい書き方になります。ちょっとVueやAngularっぽいと思う人もいるかもしれません。keyが要らないのにも注目してください。

import { For } from 'react-loops'

function List() {
  const itemList = ['a', 'b', 'c']

  return (
    <ul>
      <For of={itemList}>{item => <li>{item}</li>}</For>
    </ul>
  )
}

配列が空の場合を考慮したリスト

配列が空の場合は、"no items" と出したいとしましょう。

普通のJSX

    <ul>
      {itemList.length < 1 ? (
        <div>no items</div>
      ) : (
        itemList.map(item => <li>{item}</li>)
      )}
    </ul>

あれあれ、ちょっと汚くなってきました。

react-loops

    <ul>
      <For of={itemList} ifEmpty={<div>no items</div>}>
        {item => <li>{item}</li>}
      </For>
    </ul>

美しくなりました!
ifEmptyにstringやcomponentを指定すると、配列が空のときはループを回す代わりに表示してくれます。

何が良い?

1. 非Reactな人にわかりやすい

日本国内においては最近はVueを勉強してからReactに来る人が多かったり、Railsなどでテンプレートエンジンに慣れ親しんだ人がReactを触る機会が多いと思います。そういった場合にほとんどのテンプレートエンジンがサポートしているループ表示にちかいインターフェースはわかりやすいです。

2. なんだかんだこういうの欲しかった

Reactエンジニアとしてはどうでしょう。自分はかなりReact的な思想に共感してVueでもAngularでもなくReactを楽しみ、好んで書いています。素のJSでもできるのにわざわざ入れる必要のない「おせっかいなライブラリ」はSimpleが強みのReactらしくない部分があり嫌いでした。
ただ、react-loopsはやっていることは単純だけど便利で個人的にはRight Level of Abstractionな気がします。

3. 痒いところに手が届くAPI

Loop iteration metadataがなかなかに素晴らしいです。

  • index
  • length
  • key
  • isFirst
  • isLast

実際のアプリケーション開発をしていると、「配列の最後だけ、最初だけXXしたい」とか、ふと配列のlengthが欲しくなったりするものですが、そういったときにreact-loopsのLoop iteration metadataは痒いところに手が届くAPIとして優秀です。

    <ul>
      <For of={itemList} ifEmpty={<div>no items</div>}>
        {(item, {isLast}) => (
          <li>{item} {isLast && '最後'}</li>
        )}
      </For>
    </ul>

こんな感じ。なんだかんだで素のJSだとどう書いてもごちゃっとなりがちです。「アプリケーションが複雑なのが悪い」と文句を言って跳ね返すのは簡単ですが、UX向上を考えるとこの手の対応は必要ですし、そうした時にコードが汚くなる後ろめたさがないのはDXも良いと感じます。

Reactぽくなくない?

同じFAQがgithubのREADMEにありますが、自分も最初見たときは「Reactぽくないからだめじゃん」と思ってました。ただ、少し使ってから考えてみるとAPIはシンプルでわかりやすいし、ただ単純にmapで配列を回す部分にそこまで素のJSであるメリットはないのではという考えに至りました。

雑感、あとがき

Reactエコシステムは日々新しい話題やアイディアが出てくるOSSのるつぼです。OSSの1ユーザーであるエンジニアにとっては取捨選択が難しいですが、こういったシンプルでエレガントなOSSを見つけるのは宝を発見したような感覚で楽しいと感じます。

今まで他に自分がシンプルかつエレガントと思ったのはstyled-components, redux-thunk, immer, react-testing-libraryあたりですが、今回新たにreact-loopsが加わりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネントのライフサイクル

React で使われているコンポーネントは、プロパティやステートの変化を受けて状態を変化します。最終的にぺージから破棄されるまでの間をコンポーネントのライフサイクルと言われます。

Reactでは、ライフサイクルの辺ごと、様々なメソッドが呼びだれています。
これにより、コンポーネントの表示や破棄の各タイミングで、独自の振る舞いを実装することができます。

以下がライフサイクル時に呼ばれるメソッド群です。

componentWillMount : コンポーネントの描画の直前に呼びばれる。
componentDidMount :コンポーネントの描画の直後に呼ばれる。
componentWillUnmount :コンポーネントの解放のタイミングで呼ばれる。
componentWillReceiveProps:プロパティを受け取る直前に呼ばれる。
shouldComponentUpdate コンポーネントの更新の可否を決定する
componentWillUpdate コンポーネントの更新の直前に呼ばれる。
componentDidUpdate コンポーネントの更新の直後に呼ばれる。

このうちは、componentWillMount :は、コンストラクタで代用可能であり、非推奨とされているらしいです。
コンポーネントのライフサイクルは大きく2つに分類され例外の処理をハンドリングすることもできます。実行例は追々。
コンポーネントのMount(生成および破棄)
コンポーネントのUpdate(更新)

コンポーネントのErrorHandling (例外発生)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む