20190816のReactに関する記事は11件です。

React Hooks の状態を iframe との間で同期したい

例えばユーザーがHTMLを書けるエディタを実装するような時は、セキュリティの都合により描画先を iframe に隔離する必要があります。そういう場合は大概 iframe 内のロジックも複雑になるので、なるべく扱いやすい形で開発したいわけです。

フロントエンドを React で開発しているのであれば、親子フレームそれぞれの特定 Component 間で状態が共有できると良さそうな気がします。

export const Parent: React.FC = () => {
  const [state, setState] = React.useState({}) // 親の状態と
}

export const Child: React.FC = () => {
  const [state, setState] = React.useState({}) // 子の状態を同期したい
}

単純に state を送信しあって同期する戦略は、state が大きくなると通信やレンダリングのコストが嵩むため避けたいところ。state 全体ではなく更新のための情報を使って同期するのであれば、React Hooks の useReducer で状態を管理して actions をやり取りするのが素直な方法でしょう。

ページロード時にだけ state 全体を同期して、その後は dispatch を呼ぶと他フレーム上でも同じ action で dispatch が実行されるようにする。これでフレーム間の通信を意識しないで開発ができそうです。Component での使い方は次のような感じ。

import React from 'react'
import { ParentProxy, ChildProxy, useProxyReducer } from './proxy'

type State = {
  greeting: string
}

type Action = { type: 'sleep' } | { type: 'meet'; name: string }

function reducer(state: State, action: Action) {
  switch (action.type) {
    case 'sleep':
      return { greeting: 'Good night!' }
    case 'meet':
      return { greeting: `Hi, ${action.name}!` }
  }
}

const initialState = { greeting: 'Good morning!' }

const parentProxy = new ParentProxy<State, Action>(
  'https://iframe-origin',
  'child-iframe' // iframe の id
)

// 親フレームの Component
const Parent: React.FC = () => {
  const [state, dispatch] = useProxyReducer(parentProxy, reducer, initialState)

  return (
    <div>
      <p>parent: {state.greeting}</p>
      <p>
        <button onClick={() => dispatch({ type: 'sleep' })}>Go to bed</button>
      </p>
      <iframe id="child-iframe" title="Child" src="/iframe.html"></iframe>
    </div>
  )
}

const childProxy = new ChildProxy<State, Action>('https://parent-origin')

// 子フレームの Component
const Child: React.FC = () => {
  const [state, dispatch] = useProxyReducer(childProxy, reducer, initialState)

  return (
    <div>
      <p>child: {state.greeting}</p>
      <p>
        <button onClick={() => dispatch({ type: 'meet', name: 'Bob' })}>
          Say hello to Bob
        </button>
      </p>
    </div>
  )
}

これを実現する実装は次のような感じで。useProxyReducer 内で useEffect を使って message イベントを listen し、postMessage 処理を差し込んだ dispatch を返します。

proxy.ts
import React from 'react'

type Action = { type: string }
// 初期状態を同期するためのアクション
export type ProxyAction<S> = { type: 'setState'; state: S }

// postMessage でやり取りするメッセージ
type Message<S, A> =
  // 親フレーム <-> 子フレーム
  // 他方で呼ばれたアクションをこちらでも実行する
  | { type: 'dispatch'; action: A }
  // 親フレーム <- 子フレーム
  // 初期状態同期のために state 全体をリクエスト
  | { type: 'sendState' }
  // 親フレーム -> 子フレーム
  // sendState メッセージのレスポンス
  | { type: 'receiveState'; state: S }

abstract class Proxy<S, A extends Action> {
  // eslint-disable-next-line no-useless-constructor
  constructor(private targetOrigin: string) {}

  // メッセージ送信先 window
  protected abstract getTargetWindow(): Window | null

  protected abstract handleMessage(
    message: Message<S, A>,
    dispatch: (action: A | ProxyAction<S>) => void
  ): void

  protected postMessage(message: Message<S, A>) {
    const target = this.getTargetWindow()
    if (target) {
      target.postMessage(message, this.targetOrigin)
    }
  }

  // 接続開始
  subscribe(dispatch: (action: A | ProxyAction<S>) => void) {
    const onMessage = (event: MessageEvent) => {
      const message = event.data as Message<S, A>
      if (message.type === 'dispatch') {
        return dispatch(message.action)
      } else {
        this.handleMessage(message, dispatch)
      }
    }
    window.addEventListener('message', onMessage)
    return () => {
      window.removeEventListener('message', onMessage)
    }
  }

  // dispatch した後にこれを呼ぶと他フレームでも実行される
  proxyDispatch(action: A) {
    this.postMessage({ type: 'dispatch', action })
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  setState(state: S) {}
}

// 親フレーム用プロキシ
export class ParentProxy<S, A extends Action> extends Proxy<S, A> {
  // 子フレームから同期のために要求された時に送る state。
  private state?: S

  // state が更新されたときは常に呼ぶ
  setState(state: S) {
    this.state = state
  }

  constructor(targetOrigin: string, private iframeId: string) {
    super(targetOrigin)
  }

  protected getTargetWindow() {
    const iframe = window.document.getElementById(this.iframeId)
    if (!iframe) return null
    return (iframe as HTMLIFrameElement).contentWindow
  }

  protected handleMessage(message: Message<S, A>) {
    switch (message.type) {
      case 'sendState':
        // 子フレームからの初期状態要求に答える
        this.sendState()
    }
  }

  subscribe(dispatch: (action: A | ProxyAction<S>) => void) {
    const unsubscribe = super.subscribe(dispatch)
    // 接続時に子フレームに同期させる
    this.sendState()
    return unsubscribe
  }

  // 子フレームに状態を送る
  private async sendState() {
    this.postMessage({
      type: 'receiveState',
      state: await this.getState()
    })
  }

  // subscribe 時には state がセットされていない可能性があるため、セットされるまで待つ
  private async getState(): Promise<S> {
    return new Promise(resolve => {
      const wait = () => {
        if (this.state) {
          return resolve(this.state)
        }
        setTimeout(wait, 100)
      }
      wait()
    })
  }
}

export class ChildProxy<S, A extends Action> extends Proxy<S, A> {
  protected getTargetWindow() {
    if (window.parent === window.self) return null
    return window.parent
  }

  protected handleMessage(
    message: Message<S, A>,
    dispatch: (action: A | ProxyAction<S>) => void
  ) {
    switch (message.type) {
      case 'receiveState':
        // 親から state が帰ってきたので同期する
        dispatch({ type: 'setState', state: message.state })
    }
  }

  subscribe(dispatch: (action: A | ProxyAction<S>) => void) {
    const unsubscribe = super.subscribe(dispatch)
    // 接続時に同期要求
    this.postMessage({ type: 'sendState' })
    return unsubscribe
  }
}

export function useProxyReducer<S, A extends Action>(
  proxy: Proxy<S, A>,
  reducer: (state: S, action: A) => S,
  initialState: S
): [S, (action: A) => void] {
  function proxyReducer(state: S, action: A | ProxyAction<S>): S {
    // 同期 action 処理を追加
    if (action.type === 'setState') {
      return (action as ProxyAction<S>).state
    }
    return reducer(state, action as A)
  }

  const [state, dispatch] = React.useReducer(proxyReducer, initialState)

  // 接続
  React.useEffect(() => {
    return proxy.subscribe(dispatch)
  }, [proxy, dispatch])

  // state の変更を反映(直接 state を同期するのは接続時のみ)
  React.useEffect(() => {
    proxy.setState(state)
  }, [proxy, state])

  return [
    state,
    action => {
      dispatch(action)
      proxy.proxyDispatch(action)
    }
  ]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooks の状態を iframe 内の Component との間で同期したい

例えばユーザーがHTMLを書けるエディタを実装するような時は、セキュリティの都合により描画先を iframe に隔離する必要があります。そういう場合は大概 iframe 内のロジックも複雑になるので、なるべく扱いやすい形で開発したいわけです。

フロントエンドを React で開発しているのであれば、親子フレームそれぞれの特定 Component 間で状態が共有できると良さそうな気がします。

export const Parent: React.FC = () => {
  const [state, setState] = React.useState({}) // 親の状態と
}

export const Child: React.FC = () => {
  const [state, setState] = React.useState({}) // 子の状態を同期したい
}

単純に state を送信しあって同期する戦略は、state が大きくなると通信やレンダリングのコストが嵩むため避けたいところ。state 全体ではなく更新のための情報を使って同期するのであれば、React Hooks の useReducer で状態を管理して actions をやり取りするのが素直な方法でしょう。

ページロード時にだけ state 全体を同期して、その後は dispatch を呼ぶと他フレーム上でも同じ action で dispatch が実行されるようにする。これでフレーム間の通信を意識しないで開発ができそうです。Component での使い方は次のような感じ。

import React from 'react'
import { useProxyReducer } from './proxy'

type State = {
  greeting: string
}

type Action = { type: 'sleep' } | { type: 'meet'; name: string }

function reducer(state: State, action: Action) {
  switch (action.type) {
    case 'sleep':
      return { greeting: 'Good night!' }
    case 'meet':
      return { greeting: `Hi, ${action.name}!` }
  }
}

const initialState = { greeting: 'Good morning!' }

const parentProxy = {
  master: true,
  targetOrigin: '*',
  targetWindow: () => {
    const iframe = window.document.getElementById('child-iframe')
    if (!iframe) return null
    return (iframe as HTMLIFrameElement).contentWindow
  }
}

// 親フレームの Component
const Parent: React.FC = () => {
  const [state, dispatch] = useProxyReducer(parentProxy, reducer, initialState)

  return (
    <div>
      <p>parent: {state.greeting}</p>
      <p>
        <button onClick={() => dispatch({ type: 'sleep' })}>Go to bed</button>
      </p>
      <iframe id="child-iframe" title="Child" src="/iframe.html"></iframe>
    </div>
  )
}

const childProxy = {
  targetOrigin: '*',
  targetWindow: () => window.parent
}

// 子フレームの Component
const Child: React.FC = () => {
  const [state, dispatch] = useProxyReducer(childProxy, reducer, initialState)

  return (
    <div>
      <p>child: {state.greeting}</p>
      <p>
        <button onClick={() => dispatch({ type: 'meet', name: 'Bob' })}>
          Say hello to Bob
        </button>
      </p>
    </div>
  )
}

これを実現する実装は次のような感じで。useProxyReducer 内で useEffect を使って message イベントを listen し、postMessage 処理を差し込んだ dispatch を返します。

proxy.ts
import React from 'react'

type Proxy = {
  master?: boolean // 接続時、master 側の state を他フレームにコピーする
  targetOrigin: string
  targetWindow: () => Window | null
}

type Action = { type: string }
// 初期状態を同期するためのアクション
export type ProxyAction<S> = { type: 'setState'; state: S }

// postMessage でやり取りするメッセージ
type Message<S, A> =
  // master <-> slave
  // 他方で呼ばれたアクションをこちらでも実行する
  | { type: 'dispatch'; action: A }
  // master <- slave
  // 同期のために state 全体をリクエスト
  | { type: 'sendState' }
  // master -> slave
  // sendState メッセージのレスポンス
  | { type: 'receiveState'; state: S }

function postMessage<S, A>(proxy: Proxy, message: Message<S, A>) {
  const target = proxy.targetWindow()
  if (target) {
    target.postMessage(message, proxy.targetOrigin)
  }
}

// 接続
function subscribe<A, S>(
  proxy: Proxy,
  dispatch: (action: A | ProxyAction<S>) => void,
  sync: () => void
) {
  const onMessage = (event: MessageEvent) => {
    const message = event.data as Message<S, A>
    switch (message.type) {
      case 'dispatch':
        return dispatch(message.action)
      case 'sendState':
        return sync()
      case 'receiveState':
        return dispatch({ type: 'setState', state: message.state })
    }
  }
  window.addEventListener('message', onMessage)

  if (proxy.master) {
    sync()
  } else {
    postMessage(proxy, { type: 'sendState' })
  }

  return () => {
    window.removeEventListener('message', onMessage)
  }
}

// master から slave に state をコピーする
function sync<S>(proxy: Proxy, state: S) {
  postMessage(proxy, { type: 'receiveState', state })
}

function proxyDispatch<A>(proxy: Proxy, action: A) {
  postMessage(proxy, { type: 'dispatch', action })
}

export function useProxyReducer<S, A extends Action>(
  proxy: Proxy,
  reducer: (state: S, action: A) => S,
  initialState: S
): [S, (action: A) => void] {
  function proxyReducer(state: S, action: A | ProxyAction<S>): S {
    // 同期 action 処理を追加
    if (action.type === 'setState') {
      return (action as ProxyAction<S>).state
    }
    return reducer(state, action as A)
  }
  const [state, dispatch] = React.useReducer(proxyReducer, initialState)

  // master 側の needSync を true にすると同期を実行する
  const [needSync, setNeedSync] = React.useState(false)

  React.useEffect(() => {
    return subscribe(proxy, dispatch, () => setNeedSync(true))
  }, [dispatch]) // eslint-disable-line react-hooks/exhaustive-deps

  React.useEffect(() => {
    if (needSync) {
      sync(proxy, state)
      setNeedSync(false)
    }
  }, [needSync]) // eslint-disable-line react-hooks/exhaustive-deps

  return [
    state,
    action => {
      dispatch(action)
      proxyDispatch(proxy, action)
    }
  ]
}

React Hooks を使ってスッキリ書けましたというのはまあ間違っていないんですが、状態を useState に押し込めてやりくりするあたり頭の体操めいた感が無きにしも非ず。

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

【React + Redux + Firebase Authentication】Promises must be handled appropriately のエラー対応

事象

以下のコードにおいて、

Promises must be handled appropriately

のエラー文にぶち当たりました。

src/containers/Auth.tsx
// 中略

const mapStateToProps = (state: StateProps) => ({
  isAuth: state.isAuth
})

const mapDispatchToProps = (dispatch: DispatchProps) => {
  return {
    dologin: () => {
      let provider = new firebase.auth.GoogleAuthProvider()
      firebase.auth().signInWithPopup(provider)
    },
    refLogin: () => {
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          return
        }
        dispatch(loginOk(user))
      })
    }
  }
}

// 中略

解決策

以下のコードを、firebase.auth().signInWithPopup(provider) の下に追加してあげます。

.then(user => {
          console.log(user)
        })
        .catch(error => {
          console.log(error)
        })

こちらが追加後のコードです。

src/containers/Auth.tsx
// 中略

const mapStateToProps = (state: StateProps) => ({
  isAuth: state.isAuth
})

const mapDispatchToProps = (dispatch: DispatchProps) => {
  return {
    dologin: () => {
      let provider = new firebase.auth.GoogleAuthProvider()
      firebase
        .auth()
        .signInWithPopup(provider)
        .then(user => {
          console.log(user)
        })
        .catch(error => {
          console.log(error)
        })
    },
    refLogin: () => {
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          return
        }
        dispatch(loginOk(user))
      })
    }
  }
}

// 中略

これによってエラーが解消されました。

少しでも何かの参考になれば幸いです!

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

【Redux + Firebase Authentication】Promises must be handled appropriately のエラー対応

事象

以下のコードにおいて、

Promises must be handled appropriately

のエラー文にぶち当たりました。

src/containers/Auth.tsx
// 中略

const mapStateToProps = (state: StateProps) => ({
  isAuth: state.isAuth
})

const mapDispatchToProps = (dispatch: DispatchProps) => {
  return {
    dologin: () => {
      let provider = new firebase.auth.GoogleAuthProvider()
      firebase.auth().signInWithPopup(provider)
    },
    refLogin: () => {
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          return
        }
        dispatch(loginOk(user))
      })
    }
  }
}

// 中略

解決策

以下のコードを、firebase.auth().signInWithPopup(provider) の下に追加してあげます。

.then(user => {
          console.log(user)
        })
        .catch(error => {
          console.log(error)
        })

こちらが追加後のコードです。

src/containers/Auth.tsx
// 中略

const mapStateToProps = (state: StateProps) => ({
  isAuth: state.isAuth
})

const mapDispatchToProps = (dispatch: DispatchProps) => {
  return {
    dologin: () => {
      let provider = new firebase.auth.GoogleAuthProvider()
      firebase
        .auth()
        .signInWithPopup(provider)
        .then(user => {
          console.log(user)
        })
        .catch(error => {
          console.log(error)
        })
    },
    refLogin: () => {
      firebase.auth().onAuthStateChanged(user => {
        if (!user) {
          return
        }
        dispatch(loginOk(user))
      })
    }
  }
}

// 中略

これによってエラーが解消されました。

少しでも何かの参考になれば幸いです!

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

Elm勉強会@弊社(2019/08/28)

Elmとは

  • フロントエンド開発に特化したプログラミング言語
  • JavaScriptにコンパイルされて実行される、いわゆるAltJS
  • 2012年に登場した、比較的若い言語
  • 静的型付言語&純粋関数型言語
  • 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)

Elmはフレームワーク内蔵言語

React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。

ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。


JavaScriptとの違い


変数について 〜JavaScriptの場合〜

JavaScriptでは、varletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。

JavaScript
var a = 1;
a = 2

let b = 1;
b = 2;

const c = 1;
c = 2; // エラー!

変数について 〜Elmの場合〜

Elmの場合はvarletconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。

Elm
a = 1
a = 2
-- コンパイルエラー!

そもそも代入再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。


オブジェクト 〜JavaScriptの場合〜

constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。

JavaScript
const takashi = {
    name: "たかし",
    age: 36,
};

takashi.age = 37; // ageプロパティを上書き。

レコード 〜Elmの場合〜

Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。

Elm
takashi =
    { name = "たかし"
    , age = 36
    }

newTakashi =
    { takashi | age = 37 } -- 上書きでなく新しいレコードが生成されます。

レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということはできません。

上記のコードも、一部を変えた新しいレコードが作り出されます。

元のレコード(takashi)は36歳のまま残ります。


書く順序による影響 〜JavaScriptの場合〜

↓エラーにならないパターン

JavaScript
const a = 3;
const b = 5;

const c = a + b;

↓エラーになるパターン

JavaScript
const c = a + b;

const a = 3;
const b = 5;

abに値を代入するより上の行で、abを使った計算などをしようとするとエラーになります。


書く順序による影響 〜Elmの場合〜

Elm
c = a + b

a = 3
b = 5

ab定義するより上の行abを計算に使うことができます。


JavaScriptでは状態の変化を直接コードに書くことができる

JavaScript
let takashi_age = 36;
console.log(takashi_age); // 36

takashi_age = 37;
console.log(takashi_age); // 37

takashi_age = 38;
console.log(takashi_age); // 38

takashi_age = 39;
console.log(takashi_age); // 39

JavaScriptの場合は変数の値を変更できるので、例えばconsole.logtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。


Elmでは、場面や状態の変化を直接コードで書けない

再代入という概念がなく、コード内の全ての値が不変だからです。

Elm
season = "夏"
-- ずーっと夏

age = 36
-- ずーっと36歳

そのため「この行でageを呼び出したら36だけど、もう少し下の行で呼び出したら37だった」ということがありません。

再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。

一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。


再代入できないことによるメリット

  • 違うものには違う名前がつくので明解
  • この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
  • 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
  • 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽

関数 〜JavaScriptの場合〜

JavaScriptでは↓こんな感じで関数を宣言します。

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

また、無名関数を変数に格納するパターンもあります。

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

アロー関数式で書く場合は↓こうです。

JavaScript
const add = (a, b) => a + b;

実行するときは↓こうですね。

JavaScript
const result = add(3, 5);

関数 〜Elmの場合〜

Elmでは↓こんな感じで関数を定義します。

Elm
add a b =
    a + b

実行するときは

Elm
result = add 3 5

Elmではカッコカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。

関数と変数の境目があまりない感じです。
引数があれば関数です。


関数の返す値も(引数が同じならば)常に一定

それは、全ての値やレコードが不変で、場面状態といったものを直接コードで書くことができないからです。

Elm
scene =
    "朝"


greeting name =
    if scene == "朝" then
        name ++ "さん、おはよう!"

    else
        name ++ "さん、こんにちわ!"

再代入できないためsceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。


Elmは何も変えられない・・・?

Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。

具体的には、JavaScriptでいう、

JavaScript
doSomething(a, b);

的な「戻り値を使わない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を受け取って利用しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして戻り値を使わない、というコードを書くことはありません。

JavaScript
const result = someCalculate(a, b);

上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」という定義をするような書き方がメインになります。


何も変えられなくて、この言語なにができるの?

定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。

しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。


デモアプリをみてみましょう


デモアプリ

ブラウザエディタEllieでご確認ください。

※好きにコードをいじってもらって大丈夫です!
(私のコードとは別に保存されるので)


定義だけで状態変化を表現できました

しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。


オブジェクト指向との違い


オブジェクト指向とは

データと関数(メソッド)をまとめて定義した「クラス」つまりを定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界のモノを認識するときの考え方に似ているため、比較的直感的にプログラミングできるという、非常に強力なスタイル。

※諸説ありますが、ここでは上記の認識で進めます。


クラスを使って書いてみる

JavaScript
class Human {
    constructor (name, age) {
        this.name = name;
        this.age = age;
    }

    increment () {
        this.age++;
    }

    decrement () {
        this.age--;
    }
}

const takashi = new Human("たかし", 36);

takashi.increment();
takashi.increment();

console.log(takashi.age); // 38

関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。


Elmでも、オブジェクト指向っぽく考えることもできる

ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。

Elm
addAge : Int -> Human -> Human -- 型注釈
addAge int human =
    { human | age = human.age + int }

Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。


コンポーネント指向との違いは?

divbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。

Elm
buttonComponent props =
    button [ class "common-btn" ] [ text props.text ]

これってReactやVueのFunctional Componentですよね。


The Elm Architectureについて


ReduxやVuexの元となった手法

model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。

コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。

ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。

再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。

もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。

そして、環境構築のコストもかなり低めです。


型システムが優秀なため、修正にも強い

例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。

リセット機能追加の流れ

  • リセットボタンをview関数の中に追加
  • そのボタンをクリックしたらResetというメッセージが発生するように属性を追加
  • Resetなんて知らないよ!とエラーが出る
  • メッセージの型にResetを追加
  • Resetのケースも書かないと!とエラーが出る

エラーメッセージが分かりやすいのもElmの特徴です。特に、機能追加をしている時などは「そこにコードを追加するなら、ここにも追加しないとでは!?」と導いてくれているような感じがします。

エラーになるようなコードはコンパイル時に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。


純粋関数型言語の特徴


副作用を直接書けない

再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。

再代入によって値を変えることで状態変化を表現することはできないため、状態を変化させたい値はmodelに組み込むことになります。
そして、状態を更新する処理はupdate関数の中に書いていきます。
コードの記述方法がある程度定まっていることで「この処理は、この辺りに書いてあるだろう」と予測しやすくなるというメリットがあります。


参照透過性が担保されている

Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。


例えば、期限付きのタスク管理システム

↑のテストをする場合のことを考えてみます。

  • タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
  • その後Webサイト側で新規タスクを作る(期限日も設定)
  • 時間操作の画面で日にちを進める。
  • そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
  • ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。

これだとテストしづらいですよね。。。


条件は全て引数として渡す

「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。


テストの自動化

例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。

そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。

そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。

Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。


ReactやVueとの比較


ReactやVueはJSで書ける

React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。

Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。


Elmなら

Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux + TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。


最後に

Elmは難しいどころか、とても入門しやすく、楽な言語です。

もちろん、複雑なものを作ろうとすればそれなりに難しいんですが、少し勉強したらピンポンゲームシングルページアプリケーションをザコーダーの私でも作ることができました。

まだまだ紹介しきれていない魅力が本当にたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)


ありがとうございました!

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

Elm勉強会@弊社

Elmとは

  • フロントエンド開発に特化したプログラミング言語
  • JavaScriptにコンパイルされて実行される、いわゆるAltJS
  • 2012年に登場した、比較的若い言語
  • 静的型付言語&純粋関数型言語
  • 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)

Elmはフレームワーク内蔵言語

React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。

ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済むので、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。


JavaScriptとの違い


変数について 〜JavaScriptの場合〜

JavaScriptでは、varletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。

JavaScript
var a = 1;
a = 2

let b = 1;
b = 2;

const c = 1;
c = 2; // エラー!

変数について 〜Elmの場合〜

Elmの場合はvarletconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。

Elm
a = 1
a = 2
-- コンパイルエラー!

そもそも代入再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。


オブジェクト 〜JavaScriptの場合〜

constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。

JavaScript
const takashi = {
    name: "たかし",
    age: 36,
};

takahsi.age = 37; // ageプロパティ上書き。

レコード 〜Elmの場合〜

Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。

Elm
takashi =
    { name = "たかし"
    , age = 36
    }

newTakashi =
    { takashi | age = 37 }

レコードも完全にイミュータブル(不変)なため、ageだけ上書きすることはできません。
一部を変えた新しいレコードを作り出します。


場面や状態の変化を直接コードで書けない

再代入できないからです!

Elm
season = "夏"
-- ずーっと夏

takashi_age = 36
-- ずーっと36歳

関数 〜JavaScriptの場合〜

JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。

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

または

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

アロー関数式で書くと

JavaScript
const add = (a, b) => a + b;

実行するときは

JavaScript
const result = add(3, 5);

関数 〜Elmの場合〜

Elmでは↓こんな感じで関数を定義します。

Elm
add a b =
    a + b

実行するときは

Elm
result = add 3 5

Elmではカッコカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。

関数と変数の境目があまりない感じです。
引数があれば関数です。


書く順序による影響 〜JavaScriptの場合〜

↓エラーにならないパターン

JavaScript
const a = 3;
const b = 5;

const c = a + b;

↓エラーになるパターン

JavaScript
const c = a + b;

const a = 3;
const b = 5;

abに値を代入するより上の行で、abを使った計算などをしようとするとエラーになります。


書く順序による影響 〜Elmの場合〜

Elm
c = a + b

a = 3
b = 5

ab定義するより上の行abを計算に使うことができます。


JavaScriptでは状態の変化を直接コードに書くことができる

JavaScript
let takashi_age = 36;
console.log(takashi_age); // 36

takashi_age = 37;
console.log(takashi_age); // 37

takashi_age = 38;
console.log(takashi_age); // 38

takashi_age = 39;
console.log(takashi_age); // 39

JavaScriptの場合は変数の値を変更できるので、takashi_age = 〇〇;の上にあるか下にあるかが重要です。


Elmでは状態変化を直接コードで表現できない

再代入ができないため「この値を、この行で呼び出したら36だけど、もう少し下の行で呼び出したら37だった」ということがありません。
再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
そのため、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行でも呼び出すことができます。

  • 違うものには違う名前がつくので明解
  • この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
  • 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
  • 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽

関数の返す値も(引数が同じならば)常に一定

それは、全ての値やレコードが不変で、場面状態といったものを直接コードで書くことができないからです。

Elm
scene =
    "朝"


greeting name =
    if scene == "朝" then
        name ++ "さん、おはよう!"

    else
        name ++ "さん、こんにちわ!"

再代入できないためsceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。


Elmは何も変えられない・・・?

Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。

具体的には、JavaScriptでいう、

JavaScript
doSomething(a, b);

的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を変数なり定数に格納しないと意味がないのです。
唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。

JavaScript
const result = someCalculate(a, b);

上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。


何も変えられなくて、この言語なにができるの?

定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。

しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。


デモアプリをみてみましょう


Elm年齢カウンター

ブラウザエディタEllieでご確認ください。


定義だけで状態変化を表現できました

しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。


オブジェクト指向との違い


オブジェクト指向とは

データと関数(メソッド)をまとめて定義した「クラス」つまりを定義して、そこからオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。

※諸説ありますが、ここでは上記の認識で進めます。


クラスを使って書いてみる

JavaScript
class Human {
    constructor (name, age) {
        this.name = name;
        this.age = age;
    }

    increment () {
        this.age++;
    }

    decrement () {
        this.age--;
    }
}

const takashi = new Human("たかし", 36);

takashi.increment();
takashi.increment();

console.log(takashi.age); // 38

コード的にも関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然です。


Elmでも、オブジェクト指向っぽくも捉えられる

ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。


コンポーネント指向との違いは?

divとかbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。


The Elm Architectureについて


ReduxやVuexの元となった手法

model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。

コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。

ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。

再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。

もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。


型システムが優秀なため、修正にも強い

例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。

リセット機能追加の流れ

  • リセットボタンをview関数の中に追加
  • そのボタンをクリックしたらResetというメッセージが発生するように属性を追加
  • Resetなんて知らないよ!とエラーが出る
  • メッセージの型にResetを追加
  • Resetのケースも書かないと!と導いてくれる

エラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。


純粋関数型言語の特徴


副作用を直接書けない

再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。
何かを変える処理はupdate関数の中で集中管理します。


参照透過性が担保されている

関数は同じ引数なら同じ戻り値を返す(外部の値に依存して振る舞いを変えない)ので、単体テスト・自動テストがしやすいです。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。

例えば期限付きのタスク管理システム

のテストをする場合に・・・

  • タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
  • その後Webサイト側で新規タスクを作る(期限日も設定)
  • 時間操作の画面で日にちを進める。
  • そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
  • ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。

なんてことになりますね。

「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、テストが書きやすくなる。
TDDにも向いています。
テストをゴリゴリに書いておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、テストでエラーが出て気づけるので安心ですよね。


ReactやVueとの比較


ReactやVueはJSで書ける

React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。

Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。


Elmなら

Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。


最後に

Elmは難しいどころか、とても入門しやすく、楽な言語です。

ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。

まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)


ありがとうございました!

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

Elm勉強会@弊社(8/28)

Elmとは

  • フロントエンド開発に特化したプログラミング言語
  • JavaScriptにコンパイルされて実行される、いわゆるAltJS
  • 2012年に登場した、比較的若い言語
  • 静的型付言語&純粋関数型言語
  • 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)

Elmはフレームワーク内蔵言語

React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。

ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。


JavaScriptとの違い


変数について 〜JavaScriptの場合〜

JavaScriptでは、varletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。

JavaScript
var a = 1;
a = 2

let b = 1;
b = 2;

const c = 1;
c = 2; // エラー!

変数について 〜Elmの場合〜

Elmの場合はvarletconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。

Elm
a = 1
a = 2
-- コンパイルエラー!

そもそも代入再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。


オブジェクト 〜JavaScriptの場合〜

constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。

JavaScript
const takashi = {
    name: "たかし",
    age: 36,
};

takahsi.age = 37; // ageプロパティ上書き。

レコード 〜Elmの場合〜

Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。

Elm
takashi =
    { name = "たかし"
    , age = 36
    }

newTakashi =
    { takashi | age = 37 }

レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということもできません。

上記のコードも、一部を変えた新しいレコードを作り出しています。

元のレコード(takashi)は36歳のままです。


書く順序による影響 〜JavaScriptの場合〜

↓エラーにならないパターン

JavaScript
const a = 3;
const b = 5;

const c = a + b;

↓エラーになるパターン

JavaScript
const c = a + b;

const a = 3;
const b = 5;

abに値を代入するより上の行で、abを使った計算などをしようとするとエラーになります。


書く順序による影響 〜Elmの場合〜

Elm
c = a + b

a = 3
b = 5

ab定義するより上の行abを計算に使うことができます。


JavaScriptでは状態の変化を直接コードに書くことができる

JavaScript
let takashi_age = 36;
console.log(takashi_age); // 36

takashi_age = 37;
console.log(takashi_age); // 37

takashi_age = 38;
console.log(takashi_age); // 38

takashi_age = 39;
console.log(takashi_age); // 39

JavaScriptの場合は変数の値を変更できるので、例えばconsole.logtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。


Elmでは、場面や状態の変化を直接コードで書けない

再代入という概念がなく、コード内の全ての値が不変だからです。

Elm
season = "夏"
-- ずーっと夏

age = 36
-- ずーっと36歳

そのため「この行でageを呼び出したら36だけど、もう少し下の行で呼び出したら37だった」ということがありません。

再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。

一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。


再代入できないことによるメリット

  • 違うものには違う名前がつくので明解
  • この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
  • 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
  • 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽

関数 〜JavaScriptの場合〜

JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。

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

または

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

アロー関数式で書くと

JavaScript
const add = (a, b) => a + b;

実行するときは

JavaScript
const result = add(3, 5);

関数 〜Elmの場合〜

Elmでは↓こんな感じで関数を定義します。

Elm
add a b =
    a + b

実行するときは

Elm
result = add 3 5

Elmではカッコカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。

関数と変数の境目があまりない感じです。
引数があれば関数です。


関数の返す値も(引数が同じならば)常に一定

それは、全ての値やレコードが不変で、場面状態といったものを直接コードで書くことができないからです。

Elm
scene =
    "朝"


greeting name =
    if scene == "朝" then
        name ++ "さん、おはよう!"

    else
        name ++ "さん、こんにちわ!"

再代入できないためsceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。


Elmは何も変えられない・・・?

Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。

具体的には、JavaScriptでいう、

JavaScript
doSomething(a, b);

的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。

JavaScript
const result = someCalculate(a, b);

上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。


何も変えられなくて、この言語なにができるの?

定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。

しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。


デモアプリをみてみましょう


デモアプリ

ブラウザエディタEllieでご確認ください。

※好きにコードをいじってもらって大丈夫です!


定義だけで状態変化を表現できました

しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。


オブジェクト指向との違い


オブジェクト指向とは

データと関数(メソッド)をまとめて定義した「クラス」つまりを定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。

※諸説ありますが、ここでは上記の認識で進めます。


クラスを使って書いてみる

JavaScript
class Human {
    constructor (name, age) {
        this.name = name;
        this.age = age;
    }

    increment () {
        this.age++;
    }

    decrement () {
        this.age--;
    }
}

const takashi = new Human("たかし", 36);

takashi.increment();
takashi.increment();

console.log(takashi.age); // 38

関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。


Elmでも、オブジェクト指向っぽく考えることもできる

ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。


コンポーネント指向との違いは?

divbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。


The Elm Architectureについて


ReduxやVuexの元となった手法

model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。

コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。

ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。

再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。

もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。


型システムが優秀なため、修正にも強い

例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。

リセット機能追加の流れ

  • リセットボタンをview関数の中に追加
  • そのボタンをクリックしたらResetというメッセージが発生するように属性を追加
  • Resetなんて知らないよ!とエラーが出る
  • メッセージの型にResetを追加
  • Resetのケースも書かないと!と導いてくれる

エラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。


純粋関数型言語の特徴


副作用を直接書けない

再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。

そのため、状態変化を表現したい値はmodelに組み込まなければなりません。また、状態を更新する処理はupdate関数の中に書かなければいけません。この縛りは一見窮屈なようですが、ルールが明確化されていることで、結果的にそれぞれの処理を探しやすくなるというメリットがあります。


参照透過性が担保されている

Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。


例えば、期限付きのタスク管理システム

↑のテストをする場合のことを考えてみます。

  • タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
  • その後Webサイト側で新規タスクを作る(期限日も設定)
  • 時間操作の画面で日にちを進める。
  • そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
  • ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。

これだとテストしづらいですよね。


条件は全て引数として渡す

「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。


テストの自動化

例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。

そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。

そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。

Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。


ReactやVueとの比較


ReactやVueはJSで書ける

React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。

Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。


Elmなら

Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。


最後に

Elmは難しいどころか、とても入門しやすく、楽な言語です。

ザコーダーの私でも、少し勉強したらピンポンゲームシングルアプリケーションを作ることができました。

まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)


ありがとうございました!

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

React Hooks と Context で手軽に状態管理がしたい

例えばダッシュボードだとか複雑なフォームだとかで局所的に Flux 的な状態管理を行いたい状況はよくあります。でもそのために Redux なりを使うのは気が重い……ということで React Hooks で作成した状態を Context で伝播させて穏便に事を済ませることにします。お手軽かつ使いやすい扱い方を検討したい。

React.useState

最もプリミティブな形の実装。親 Component で作成した state とそのセッターを Context を通じて子 Component に渡す。

import React from 'react'

type State = {
  greeting: string
}

const initialState = {
  greeting: 'Good morning'
}

const StateContext = React.createContext<[State, (state: State) => void]>([
  initialState,
  () => {}
])

const Parent: React.FC = () => {
  const [state, setState] = React.useState<State>(initialState)
  return (
    <StateContext.Provider value={[state, setState]}>
      <Child />
    </StateContext.Provider>
  )
}

const Child: React.FC = () => {
  const [state, setState] = React.useContext(StateContext)
  function changeGreeting() {
    setState({ greeting: 'Good night!' })
  }

  return <div onClick={changeGreeting}>{state.greeting}</div>
}

React.useReducer

先の例では各々の子 Component が setState を呼ぶのですが更新のロジックは1箇所にまとめたい、state を直接触るのもなー、などもう少し構造が欲しい感があります。そういうわけで React が提供している Hooks API が useReducer です。

https://reactjs.org/docs/hooks-reference.html#usereducer

リファレンスページに載っている例

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

い、いやじゃ…… action の定義や reducer の switch 文など書きとうない……。また reducer では async な処理が書けないので、そのためにもう1層噛ませることになります。ここら辺の再発明をするのであれば Redux 使えばいいんじゃないかなという気がします。

結局どうするか

子 Component から状態にアクセスする時は statesetState を wrap した関数を使う、程度でいいんじゃないでしょうか。

function useGreeting() {
  const [state, setState] = React.useContext(StateContext)

  return {
    greeting: state.greeting,
    changeGreeting() {
      setState({ greeting: 'Good night!' })
    }
  }
}

const Child: React.FC = () => {
  const { greeting, changeGreeting } = useGreeting()

  return <div onClick={changeGreeting}>{greeting}</div>
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react で "number of file watcher reached" になり yarn start できなかった (ubuntu 18.04)

ubuntu18.04でreacを使い yarn startを実行しようとした所以下のエラーになった

Error: ENOSPC: System limit for number of file watchers reached

原因

system's file watchersの上限を超えていた

対策

以下を実行してファイル監視数の上限を開放する。

$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

参考

https://github.com/gatsbyjs/gatsby/issues/11406

https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers#the-technical-details

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

JavaScript/Reactの見直し用記事

はじめに

いろんな言語をやっているとどうしても忘れてしまうのでよく忘れるものをメモ

以下の違いって何?って質問された時微妙な答えをしてしまったのでコレもメモ
フロントエンドとは?
- HTMLの構築
- バックエンドへAPIを呼び出す
- データの返却を受け取る

バックエンドとは?
- データベースとの接続
- フロントエンドからの要求に対応するデータを返すAPIを用意
- データをフロントエンドへ渡す

DOMとは

「Document Object Model」の略。HTMLとJavaScriptをつなぐ役割をもつ。
階層構造を取り、各要素を表すノードで表現されることが多い。

Node.jsとは

JavaScript アプリケーションのプラットフォームでサーバーサイドのJavaScript
ReactなどのJSフレームワークを導入する際にはNode.jsが必要となる。(Node.jsが支える側に)
Node.jsを使わずにReactを利用するには、CDN経由で読み込む。

node.jsのバージョン管理はnodebrewを使う。これはhomebrewでインストールする。
homebrew自体でもでもnodeは管理できるがおすすめされない。

npmとは

「Node Package Manager」の略。Node.jsのパッケージを管理するツールである。
nodebrewからNode.jsをインストールすると付いてくる。

yarnとは

npmと同様にJavaScriptのパッケージ管理ツール。
高速で、信頼性が高く、安全性も高い。
homebrewからインストールする。

JavaScript

1個目の書き方(関数式)

関数を宣言する際に関数名を記述しなくても良いので、無名関数や匿名関数とも呼ばれる方法

関数式
const 定数名 = function(仮引数, 仮引数, ...) {
  処理;
  return 返り値;
};
関数式
let 変数名 = function(仮引数, 仮引数, ...) {
  処理;
  return 返り値;
};
呼び出し
定数名(実引数, 実引数, ...);
const name = function(x) {
  console.log(x) // 出力: 香風智乃
};
name("香風智乃")

2個目の書き方(関数宣言)

関数をそのまま宣言することでプログラム内で利用することができるようにする方法

関数宣言
function 関数名(仮引数, 仮引数, ...) {
  処理;
  return 返り値;
}
呼び出し
関数名(実引数, 実引数, ...);
function dispMsg(str){
  console.log(str); // 出力: 保登心愛
}

dispMsg("保登心愛");

3個目の書き方(アロー関数)

ES2015(ES6)から導入され、functionを使わない代わりに、=>で関数を表現することができる

アロー関数
const 定数名 = (仮引数, 仮引数, ...) => {
  処理;
  return 返り値;
}
アロー関数
let 変数名 = (仮引数, 仮引数, ...) => {
  処理;
  return 返り値;
}
呼び出し
定数名(実引数, 実引数, ...);
const name = (x) => {
    console.log(x)
}
name("香風智乃")

参考記事

JavaScriptのthisの理解

アロー関数式

prototype

Object

method

コールバック関数

非同期処理

テンプレートリテラル

React

Create React App

create-react-app
$ npx create-react-app アプリ名
$ cd アプリ名
$ yarn start 

フォーム

なぜコールバック関数を使用する際はアロー関数を使わないとエラーになるのか

React-Redux

非同期処理

React Hooks

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

NextJSとMaterialUIでパンくずリストを作ってみる

NextJSとMaterialUIでパンくずリストを作ってみる

なかなかパンくずリストの作成って意外と面倒くさいですよね。
特にNextJSやNuxtJSだとSSRがあったりしてなかなか辛かったりもします。
日本語の記事だとNuxtJS関連のものはよく出てくるのですがNextJS関連のものはあまり出てこなくて
(´・ω・`)となったりします。
しかもNextJS9になってからはダイナミックルーティングが追加されたりとなかなかやりたいことができそうになってきそうです。
そこで今回はNextJS + MaterialUIというテーマでやってみたので忘備録です。

目指したいパンくずリストは次のようなイメージです。
/users/1/orders/1 => ユーザー一覧 / ユーザー詳細 / 注文一覧 / 注文詳細
ルーティング名がパンくずリストになるような感じです。
ユーザー詳細が実際のユーザーの名前になる実装は実は今回は考慮していません。。。

実際のイメージはMaterialUIの公式を見ていただければイメージが付くかと思います
https://material-ui.com/components/breadcrumbs/

使ったものたち

  • NextJS(ver9)
  • MaterialUI
  • TypeScript
  • yarn
    などなど...

必要な依存関係はyarn addしたものがあるので参考に

$ yarn add \
 @types/node \
 @material-ui/core \
 next \
 react @types/react react-dom \
 typescript --network-timeout 1000000 # materialUIが重いのでタイムアウトを伸ばした

ページルーティング

パンくずリストをつくっていく上で重要になってくるのがURLのパスです。
RESTっぽいとパンくずリスト向きなURLになります、ある程度秩序を持ったルーティングでも作りやすいと思います。(無秩序だと厳しいのでは...)
今回はREST前提な感じのルーティングで作っていきます。
以下のルーティングでパンくずリストを作ろうと思います。

ルーティング 画面名
/ トップ画面
/users ユーザー一覧画面
/users/[userId] ユーザー詳細画面
/users/[userId]/orders 注文一覧画面
/users/[userId]/orders/[orderId] 注文詳細画面
/items 商品一覧画面
/items/[itemId] 商品詳細画面
/items/[itemId]/edit 商品編集画面
/about このサイトについて画面?

ちなみに基本はRESTっぽい感じですが/aboutはRESTっぽくなくしています。
NextJSでダイナミックルーティングするときのパスパラメータは[xxx][xxxId]と表す(最初ビックリした)のでそれによってpagesにディレクトリとファイルをほります。

pages
├── about.tsx
├── index.tsx
├── items
│   ├── [itemId]
│   │   ├── edit.tsx
│   │   └── index.tsx
│   └── index.tsx
└── users
    ├── [userId]
    │   ├── index.tsx
    │   └── orders
    │       ├── [orderId]
    │       │   └── index.tsx
    │       └── index.tsx
    └── index.tsx

ファイルをほったあとは適当にhello world的なものを出力させるようにします。
このときLayoutコンポーネントあたりを作ってラップさせれば後の実装が楽になります。

withRouterコンポーネントについて

実装方法は色々あると思うのですが今回はwithRouterというNextJSにバンドルされているHOCを利用した実装方法です。
withRouter自体の利用方法はこんな感じです。

const MyComponent = props => {
  console.log(props.router);
  ...
};

export default withRouter(MyComponent); // ラップ

withRouterを適用するとpropsに次のような内容のものが付与されます。

{ router:
   { pathname: '/users/[userId]/orders/[orderId]',
     route: '/users/[userId]/orders/[orderId]',
     query: { userId: '1', orderId: '1' },
     asPath: '/users/1/orders/1',
     events:
      { on: [Function: on],
        off: [Function: off],
        emit: [Function: emit] },
     push: [Function],
     replace: [Function],
     reload: [Function],
     back: [Function],
     prefetch: [Function],
     beforePopState: [Function] } }

これを利用してパンくずリストを作っていきましょう。

実装例

実際に実装していきます。
ポイントはwithRouterから付与されるpathnameasPathというパラメータです。
これらのパラメータは現在のページのルーティングやパスを表しています。
加えて、これらのパラメータは対応しているため/でSplitしたあと同じIndexでアクセスすればそのセクションが取れるはずです。
そして、それらのパラメータとその前半をくっつければルーティングのいずれかにマッピングできるはずなので、そのルーティングの名前をどこかに定義しマッピングすれば実装が完了します。

つまりpathname=/users/[userId]/orders/[orderId]asPath=/users/1/orders/1でイメージしてみるとこんな感じになります。

const routingMapping = { // ルーティング名の定義を行う
  '/': { title: 'トップページ' },

  '/users': { title: 'ユーザー一覧' },
  '/users/[userId]': { title: 'ユーザー詳細' },
  '/users/[userId]/orders': { title: '注文一覧' },
  '/users/[userId]/orders/[orderId]': { title: '注文詳細' },
}

const pathname = '/users/[userId]/orders/[orderId]'.split('/').filter(i => i.length > 0);
const asPath = '/users/1/orders/1'.split('/').filter(i => i.length > 0);
// 上記の値は次のような感じになる
// pathname = ['users', '[userId]', 'orders', '[orderId]']
// asPath = ['users', '1', [orders], '1']

const links = [];
let pathnameHierarchy = '/'; // ループさせ `/` => `/users` => `/users/[userId]` => `/users/[userId]/...` と増えていく
let asPathHierarchy = '/'; // ループさせ `/` => `/users` => `/users/1` => `/users/1/...` と増えていく
const length = pathname.length;
for (let i = 0; i < length; i += 1) {
  const target = mapping[pathHierarchy]; // ルーティング名の定義を取得
  links.push(
    target ? (
      <Link color="inherit" className={classes.link} href={asPathHierarchy // hrefで実際のパスの値をリンク } key={asPathHierarchy} > 
        {target.title // 定義したルーティング名のタイトルを注入 }
      </Link>
    ) : (
      <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} >
        {asPath[i] // ルーティング名が未定義の場合は実際の値を注入 }
      </Link>
    ),
  );
  // ループでパスのお尻を増やす部分
  pathHierarchy += pathHierarchy.endsWith('/') ? pathname[i] : `/${pathname[i]}`; // 頭に`/`がついた状態で始まるのでパス名が`//users/...`となってしまう対策
  asPathHierarchy += asPathHierarchy.endsWith('/') ? asPath[i] : `/${asPath[i]}`;
}

このままだと最下層がlinksに入りませんがあえてそうしています。
実際にコンポーネントを作成するときに活用されます(変数名deepestを定義しているあたりです)。

上記を踏まえてパンくずリストコンポーネントを作ってみます。

import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import {
  Paper,
  Breadcrumbs,
  Typography,
  Link,
} from '@material-ui/core';
import { withRouter } from 'next/router';

const routingMapping = {
  '/': { title: 'トップページ' },

  '/users': { title: 'ユーザー一覧' },
  '/users/[userId]': { title: 'ユーザー詳細' },
  '/users/[userId]/orders': { title: '注文一覧' },
  '/users/[userId]/orders/[orderId]': { title: '注文詳細' },

  '/items': { title: '商品一覧' },
  '/items/[itemId]': { title: '商品詳細' },
  '/items/[itemId]/edit': { title: '商品編集' },

  '/about': { title: 'このサイトについて' },
};

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    link: {
      display: 'flex',
    },
    icon: {
      marginRight: theme.spacing(0.5),
      width: 20,
      height: 20,
    },
  }),
);

const Breadcrumbs = props => {
  const classes = useStyles(props);
  const pathname = props.router.pathname
    .split('/')
    .filter(element => element.length > 0);
  const asPath = props.router.asPath
    .split('/')
    .filter(element => element.length > 0);
  const length = pathname.length;

  const links = [];
  let pathnameHierarchy = '/';
  let asPathHierarchy = '/';
  for (let i = 0; i < length; i += 1) {
    const target = routingMapping[pathnameHierarchy];
    links.push(
      target ? (
        <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} >
          {target.title}
        </Link>
      ) : (
        <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} >
          {asPath[i]}
        </Link>
      ),
    );
    pathnameHierarchy += pathnameHierarchy.endsWith('/') ? pathname[i] : `/${pathname[i]}`;
    asPathHierarchy += asPathHierarchy.endsWith('/') ? asPath[i] : `/${asPath[i]}`;
  }
  const deepest = mapping[pathHierarchy]; // 最下層=現在のルーティングはクリックできないように<Typography>でリストを作成
  links.push(
    deepest ? (
      <Typography color="textPrimary" className={classes.link} key={asPathHierarchy} >
        {deepest.title}
      </Typography>
      ) : (
      <Typography color="textPrimary" key={asPathHierarchy}>
        {asPathHierarchy}
      </Typography>
    ),
  );
  return (
    <Paper elevation={0} >
      <Breadcrumbs separator="/" aria-label="breadcrumb">
        {links.map(link => (link))}
      </Breadcrumbs>
    </Paper>
  );
};

export default withRouter(Breadcrumbs);

あとはこれをLayoutコンポーネントあたりに付与すれば...完成です。

import React from 'react';
import ./Breadcrumbs

const Layout = props => {
  return (
    <main>
      <Breadcrumbs /> { // パンくずリストを挿入! }
      {props.children}
    </main>
  );
};

export default Layout;
import React from 'react';
import Layout from '../components/Layout';

const Index = props => {
  return (
    <Layout>
      <h1>Hello</h1>
    </Layout>
  )
}

export default Index;

デザイン周りはMaterialUIの公式から拝借しました。

所感

今回はNextJSを利用したパンくずリスト作成でしたが、この考え方を応用すれば基本的にどのようなフレームワークのパンくずリストでもページの個別実装なしに作成可能な気がします。
パンくずリスト作成に必要なものはその画面へのルーティング実際のパスだというのがわかったのは今回大きな収穫です。
やってみると面倒なパンくずリスト作成ですが、これで楽になりたいと思います。
あと、ReactやSSRってwebpack周りで敷居が高そうなイメージだったのですがそのへんをNextJSがうまくやってくれているみたいだったのでだいぶ敷居が下がったようにも感じます。
本業はバックエンドですがフロントも楽しい・ω・

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