20200917のReactに関する記事は10件です。

React Hooksについて(超基本)

はじめに

今年の1月から就活を始めたところ結果が芳しくなく、スキルの無さに悩みながらも
5~6月でチームでDjangoのWebサービスを開発するという企画に参加した際に、

「最近はフロントの知識もないときついと聞きますがTypeScriptってどれやるのがいいのかわからないし、JavascriptもAjaxとHTMLやCSSの属性変更で使ったくらいなのでハードル高いです……」

と聞いたところ、とりあえずいきなり初めて見るのも手だよとアドバイスを頂いたのでそれならばと興味のあったReactチュートリアルを終えて、Django側もAPI化しないといけないということでDRFのチュートリアルを完走して、1月に作ったポートフォリオからレベルアップし、チーム開発の成果とあわせてアピールできるように「React+Redux+CORS+axios+DRFで作るToDoアプリ」ということで要件定義から設計まで行いました。(なお、設計に関してはまだまだスキルの練度が低く、作業中に色々変えていった方が良かったりする部分もあるのであくまでも作業開始の土台として行いました)

そして、いざ実装の作業に移った結果、チュートリアルの知識だけではまるで足りずに、Reactの部分で沼にハマってしまいどうしたものかと嘆いたところTwitterでHooksについての理解があったほうがいいですよというアドバイスを頂いたので、作業を中止してReact Hooksについてドキュメントを履修してきたのでそれの覚書となります。

ちなみに各チュートリアルの覚書やチーム開発企画、ポートフォリオについてのアウトプットは以下の通りです、ご興味あれば読んでいただけると嬉しいです。

未経験からweb系エンジニアになるための独学履歴~初めてのポートフォリオ作成記録 製作記録編~
初心者がDjangoによる6週間でチームビルディングからプロダクト公開までやるプロジェクトに参加した話
Reactチュートリアルをこなしてみてその1
Reactチュートリアルやってみたその2
DRFチュートリアルを終えてみて

どこで沼にハマったのか

今回DjangoやLaravelのようなHTMLテンプレートまで用意されたフルスタックなフレームワークのみでなく、フロントとサーバー(バックエンド)で使うフレームワークを分けて何かをやるのが初めてでReact(フロント側)でも認証についての処理を書かないといけないということを知らなかったというところがことの発端でした。
ちなみに、この認証に関しては今回とは別にJWTでやるのかJWTでやるにしてもその保存方法はどうするのか? はたまたSessionでやるのか? などあまりにも(日本語での)情報が少ない上に、錯綜していて海外の各サイトのフォーラムなどを解読して……みたいなことをやって悩み悩むのですがそれは別の話です。

閑話休題、今まで認証に関しては完全フレームワーク任せだったので困った私はなにか参考になるものはないかと探してたところ

reactjs-auth-django-rest

上記のリソースが使えそうだなと思い、コードが古かったのとそのまま使うのはということで勉強も兼ねてRe-Ducksの書き方にリファクタリングしようと思って見様見真似でやっていたのですが、ログインの処理はリソースを参考にしてできたものの、上記リソースの処理の中で


export function logoutUser() {
    localStorage.removeItem("token");
    return {
        type: AuthTypes.LOGOUT
    };
}



import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { logoutUser } from "../../actions/authActions";

class Logout extends Component {

    static propTypes = {
        logoutUser: PropTypes.func.isRequired
    };

    componentWillMount() {
        this.props.logoutUser();
    }

    render() {
        return (
            <h2>Sorry to see you go...</h2>
        );
    }
}

export default connect(null, { logoutUser })(Logout);

こちらの処理をRe-Ducks方式で書き直そうとしてlogoutUser()dispacth()を使うように書き換えようとしたところdispatch is not a functionエラーが出て、それを解消できず、さらおまけにcomponentWillMount()もレガシーで書き換えることが推奨されおり、どうしたものかと頭を抱えていた……というところで冒頭に繋がります。

Hooksの基本的な考え方

// 以下のようにStateを定義する


import React, { useState, useEffect } from 'react';

function ExampleWithManyStates() {
    // age変数に42を代入、以後setAgeでage変数を変更する
    const [age, setAge] = useState(42);
    // fruit変数に'banana'を代入、以後setFruitで管理する
    const [fruit, setFruit] = useState('banana');
    // todos変数にtext: 'Learn Hooks' プロパティを代入、以後setTodosで管理
    const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
    // count変数に0を代入、以後setCountで管理
    const [count, setCount] = useState(0);
    // ...
}

// すると以下のように呼び出してStateに干渉することができる

import React, { useState, useEffect } from 'react';

function Example() {
    const [count, setCount] = useState(0);

    // Similar to componentDidMount and componentDidUpdate:
    useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`;
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

このとき useEffect()には関数を渡すことでcomponentDidMount and componentDidUpdateのようにrenderのあとに、Stateを変更する処理を加えることができる。
この場合は、onClick = {() => setCount(count + 1)}で変化したcountの分だけdocument.titleの${ count }も変わるということになる。

もっとわかりやすい例が以下である

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(status) {
        setIsOnline(status.isOnline);
    }

    useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
    });

    if (isOnline === null) {
        return 'Loading...';
    }
    // isOnlineがnullなら'Offline'を返す
    return isOnline ? 'Online' : 'Offline';
}

isOnlineは最初nullであるが、useEffectによってsetIsOnline()が呼び出され、isOnlineとなる。
なので最終的にreturnされるisOnlineはOnlineとなる。
さらにuseEffect()内でreturnで関数を返すとコンポーネントがDOMから削除された際の処理を定義できる。

つまりuseEffectとuseStateを用いたこれら一連の処理で

componentDidMount()
・ 初回のコンポーネントマウント時における挙動。例えばログイン処理などの非同期処理やタイマーのセット。

componentDidUpdate()
・ componentDidMount()以降でpropsやstateが変更された場合の処理

componentWillUnmount()
・ コンポーネントを破棄する場合の挙動。例えばログインに対するログアウト、タイマーセットに対する解除、非同期処理の中止。

というクラスコンポーネントの一連の挙動を定義できるということになる。
上記の例だとcomponentDidMount及びcomponentWillUnmountにおける挙動がuseEffectで定義されているということになる。

実際の場合だと、購読ボタンを押すとこの関数が呼び出されてrenderが始まり、subscribeToFriendStatusとなり、購読解除ボタンを押すと再度呼び出され再度renderされunsubscribeFromFriendStatusとなるといった処理になる。

また、Hookは定義したコンポーネントでしか呼び出せないが関数にすることで別のコンポーネントや関数で呼び出せる。

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(status) {
        setIsOnline(status.isOnline);

    }

    useEffect(() => {
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
    });

    return isOnline;
}

// すると上記のHookは以下のように2つの関数で使うことができる

function FriendStatus(props) {
    const isOnline = useFriendStatus(props.friend.id);

    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
    const isOnline = useFriendStatus(props.friend.id);

    return (
        <li style={{ color: isOnline ? 'green' : 'black' }}>
            {props.friend.name}
        </li>
    );
}

また、クラスコンポーネントでHookを使うことはできない。

主にこのあたりの話を中心にドキュメントは進んでいきます。

useState

例えば以下のようなHookがあるとする


import React, { useState } from 'react';

function Example() {
    // Declare a new state variable, which we'll call "count"
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
      </button>
        </div>
    );
}

これをReactのこれまでの書き方で書くと以下の通りになる。

// クラスとconstructorとset.Stateを使う例

class Example extends React.Component {
    // state初期化
    constructor(props) {
        super(props);
        this.state = {
                count: 0
        };
    }

    render() {
        return (
            <div>
                {/* 変更が反映される部分 */}
                <p>You clicked {this.state.count} times</p>
                {/* state変更フラグ */}
                <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                    Click me
                </button>
            </div>
        );

    }
}


useStateは上記のクラスコンポーネントにおいてのconstructorの役割を担う。
stateの初期値とそのstateの状態を変化させる関数をセットするイメージ。

本来ならconstructorのあとにcomponentDidMountなどの処理を定義してrenderするのでそれに相当するuseEffectがあるがそれについては後述する。
Hookはクラスではないのでthis.stateを使えない。

よってここまでをまとめると


// useStateのインポート
import React, { useState } from 'react';

function Example() {
    // useStateの定義。第1引数にState変数、第2にそれを管理する関数名を定義
    // 同時にState変数に初期値となるuseStateの引数を代入、今回はcount=0が初期値。
    const [count, setCount] = useState(0);

    return (
        <div>
            {/* setCount(count + 1)の結果が{ count }に反映される。クラスだと {this.state.count }*/}
            <p>You clicked {count} times</p>
            {/* setCount関数によりボタンが押される度にcount + 1の処理がなされる。クラスだと{ count: this.state.count + 1 } */}
            <button onClick={() => setCount(count + 1)}>
                Click me
      </button>
        </div>
    );
}

ということになる。

ちなみに上記のように値の増減でなく、状態の管理や値の置き換えでStateを使う場合以下のように定義しておく


function Example2(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    // state変数を置き換えるメソッドをHookの中に定義しておく
    function handleStatusClick(status) {
        setIsOnline(status.isOnline);
    }

    // ....
}

これは次のuseEffectなどと一緒に使うことになる。

useEffect

以下のようなHookを使ったコンポーネントがあるとする

import React, { useState, useEffect } from 'react';

function Example() {
    const [count, setCount] = useState(0);

    // Similar to componentDidMount and componentDidUpdate:
    useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`;
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
      </button>
        </div>
    );
}

これをHookを使わないで書くと以下のようになる。


class Example extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count : 0
        };
    }

    componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
    }

    componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
    }

    render () {
        return (
            <div>
                <p>You clicked {this.state.count} times</p>
                <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                    Click me
                </button>
            </div>
        );
    }
}

改めてになるがHookでやりたい事というのはconstructorで初期化と初期値セット(useStateの場合はstateを管理する関数のセットも)をして、componentDidMountでrenderされた直後に動作する処理を定義し、componentDidUpdateで初回以後のrenderの実行やstateの変化に対してどういう処理をなすかという基本的なサイクルを構築するということである。

上記のクラスコンポーネントの場合、this.stateのcountプロパティの値をボタンがクリックされた数だけ増加させ、{ this.state.count }の部分の表示を変更したいということになる。

Hookを使う場合、constructorの役割はuseStateが担うことは先程までで見てきたが、useEffectはcomponentDidMountやcomponentDidUpdateの役割を担うことになる。

ではuseEffectとはというとチュートリアルから引用すると

・ useEffect は何をするのか?

このフックを使うことで、あなたのコンポーネントがレンダリング後に何かをする必要があることをReactに伝えます。
Reactはあなたが渡した関数(ここでは「エフェクト」と呼ぶ)を記憶し、DOMの更新を行った後にそれを呼び出します。
このエフェクトでは、ドキュメントタイトルを設定していますが、データの取得や他の必須APIを呼び出すこともできます。

・ useEffectはなぜコンポーネントの内部で呼ばれるのか?

コンポーネント内部に useEffect を配置すると、エフェクトからカウント状態変数(または任意のプロップ)に直接アクセスすることができます。それを読み取るための特別なAPIは必要ありません - それはすでに関数スコープ内にあります。フックは jsx のクロージャを採用し、jsx がすでに解決策を提供しているような React 固有の API を導入することを避けています。

・ useEffectはすべてのレンダリングの後に実行されますか?

デフォルトでは、最初のレンダリングの後と更新の後の両方で実行されます。(これをカスタマイズする方法については後ほど説明します。) 「マウント」と「更新」という観点で考えるよりも、エフェクトは「レンダリング後」に実行されると考えた方がわかりやすいかもしれません。React はエフェクトを実行するまでに DOM が更新されていることを保証します。

ということになる。

useEffectを使う際、特にComponentを破棄した場合、componentDidMountを使った処理を解除したい時はcomponentWillUnmountの処理を加えないといけない。
例えば、以前も書いたユーザーのアクティブを管理するという処理をHookを使わないで書くと以下のようになる。

class FriendStatus extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isOnline: null };
        this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
        ChatAPI.subscribeToFriendStatus(
            this.props.friend.id,
            this.handleStatusChange
        );
    }
    componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
            this.props.friend.id,
            this.handleStatusChange
        );
    }
    handleStatusChange(status) {
        this.setState({
            isOnline: status.isOnline
        });
    }

    render() {
        if (this.state.isOnline === null) {
            return 'Loading...';
        }
        return this.state.isOnline ? 'Online' : 'Offline';
    }
}

これをHookで書くとこうなる。


import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
    // constructorに相当
    const [isOnline, setIsOnline] = useState(null);

    // レンダリング後に実行される処理
    useEffect(() => {
        function handleStatusChange(status)
        return () => {
            setIsOnline(status.isOnline);
        }
        // FriendStatusクラスのcomponentDidMountに相当
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        // FriendStatusクラスのcomponentWillUnmountに相当
        return () => {
            ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange);
        };
    });

    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

// useEffect内でreturnで関数を返すようにするのが肝
// 当然componentWillUnmountが必要のない処理の場合は返す必要はない

Hookの分割とHookの処理順について

例えば以下のようなコンポーネントがあったとする。


class FriendStatusWithCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count : 0,
            isOnline : null
        };
        this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
        ChatAPI.subscribeToFriendStatus(
            this.props.friend.id,
            this.handleStatusChange
        );
    }

    componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
    }

    componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
            this.props.friend.id,
            this.handleStatusChange
        );
    }

    handleStatusChange(status) {
        this.setStatus({
            isOnline: status.isOnline
        });
    }
}

これは先程まで例に出してきたコンポーネントになるが、これだとdocument.title = 'You clicked ${this.state.count} times' がcomponentDidMountとcomponentDidUpdateとで重複し、subscribeToFriendStatus及びunsubscribeFromFriendStatusのロジックがcomponentDidMountとcomponentWillUnmountとで重複してしまっている。

これを解消しようと、useEffectを使うとどうなるかというと

function FriendStatusWithCounter(props) {
    // document.title = `You clicked ${this.state.count} times`
    const [count, setCount] = useState(0);
    useEffect(() => {
        document.title = `You clicked ${count} times`;
    });

    // subscribeToFriendStatus & unsubscribeFromFriendStatus
    const [isOnline, setIsOnline] = useState(null);
    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }

        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
    });
    // ...
}

このように複数のHookに分割して表現することできる。処理順は指定の順番もといデフォルトでは定義した順となる。
では、もう少しこの処理について詳しく見てみる。
まず、componentDidUpdateをなぜ定義するのか? ということについてになるが、それについてまずクラスコンポーネントにおける以下の箇所に注目してみる。

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

この状態だと一見問題ないように見えるが、コンポーネントがマウントされている際にprops.friend.idが変更、つまりisOnline = nullへと戻ってオフラインになると画面上にはこれを更新するメソッドがないのでオンライン表示のままである。
それは問題であるし、コンポーネントを破棄した際にメモリリークやクラッシュの原因にもなる。

これを解決するためのがcomponentDidUpdateになる。
componentDidMountが初回のrender時に実行する処理を書くのに対して、こちらはそれ以降propsまたはstateが変更されたときに実行する処理を書く。

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}


componentDidUpdate(prevProps) {
    // prePropsを引数にして直前のprops.friend.idをunsubscribeする。
    ChatAPI.unsubscribeFromFriendStatus(
        preProps.friend.id,
        this.handleStatusChange
    );

    // props.friend.idを改めてsubscribeする。
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

これをHookで書くとこうなる。


function  FriendStatus(props) {
    const [isOnline, setIsOnline] = useState(null);
    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline)
        }
        ChatAPI.subscribeToFriendStatus(
            props.friend.id, handleStatusChange
        );
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
    });
}

useEffect はデフォルトで更新を処理し、次のuseEffectを適用する前に、前のuseEffectをクリーンアップする。
その処理過程は以下の通りになる。

// 最初のマウントで { friend: { id: 100 } } がpropに渡される
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// 更新があった場合  { friend: { id: 100 } } が削除され、 { friend: { id: 200 } } がpropsに渡される
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// U上記の処理と同様に  { friend: { id: 300 } } がpropsに渡される。
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

ただし、上記の場合の例はAPIとの通信がないほか、ある1人のユーザーがオンラインかどうかの管理であるはずなので、あくまで挙動の理解のための処理いうことで理解を留めておく。
似た例でより実践的なものは後述する。

また、ここまでの過程を見るとif文で制御するべきでは? と感じました。
Reactのライフサイクルの考え方の中でもif文は使えるのでそれを見ていきます。


componentDidUpdate(prevProps) {
    // this.state.countが変動していないときはDクリーンアップを行わない。
    if(prevState.count !== this.state.count) {
        document.title = `You clicked ${this.state.count} times`;
    }
}

// これをHookで書くと以下のようになる。

useEffect(() => {
    document.title = 'You clicked ${count} times';
}, [count]);

第2引数の[count]の部分でコンポーネントがレンダリングを比較することになる。
例えばcount = 5 だとしてそのまま変動がなかったとする。
となると前後のレンダリングの結果はdocument.title = 'You clicked ${5} times' と変わらないので更新の処理はスキップされる。

では、先程までのケースでみるとどうなるだろうか。


useEffect(() => {
    function handleStatusChange(status) {
        setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(
        prop.friend.id, handleStatusChange
    );
    return () => {
        ChatAPI.unsubscribeFromFriendStatus(
            props.friend.id, handleStatusChange
        );
    };
}, [props.friend.id]);

これでprops.friend.idに変更がない場合はcomponentDidUpdateに当たる部分の処理は行われない。
もちろんコンポーネントを破棄する際のreturn以下の処理は行われる。

ここで、注意しなければならないのuseEffectで使用されるコンポーネントのスコープのすべての値(propsやstateなど)が第2引数の配列に含まれていないといけないこと。
そうしないと、直前のレンダリングではなく、さらに古い値を参照することになり適切に比較することができない。

また、useEffectを実行して一度だけ(マウント時とアンマウント時に)クリーンアップしたい場合は、第二引数に空の配列([])を渡すことができる。
これは、useEffectがpropsやStateからの値に依存せず、再実行する必要がないことをReactに伝えている。これは特別なケースとして扱われるわけではなく、依存性配列が常にどのように動作するかに直接従っているということらしい。
つまり、この場合useEffectは初回のみの実行で以降再renderされることはないということになる。
空の配列([])を渡すと、useEffect内のpropsやstateは常に初期値を保つ。

Hooksを使うためのルール

大まかには以下の3点。

  • ループや条件、入れ子になった関数の中でフックを呼び出してはいけない。
  • 常にReact関数のトップレベルでHooksを使う。
  • 通常のjsxで使わず、必ず関数コンポーネントかカスタムフックで定義をする。

ブレークポイントの設定(if文を使うときどこに定義するのか?)

改めて簡単にではあるがHooksの処理の順番を確認する。

function Form() {
    const [name, setName] = useState('Mary');

    useEffect(function persistForm() {
        localStorage.setItem('formData', name);
    });

    const [surname, setSurname] = useState('Poppins');

    useEffect(function updateTitle() {
        document.title = name + '' + surname;
    });
}


上記のようなHooksがあったとしてこれが実際に処理されるときどのように処理されるかが以下の過程である。


// ------------
// First render
// ------------
useState('Mary')           // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm)     // 2. Add an effect for persisting the form
useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle)     // 4. Add an effect for updating the title

// -------------
// Second render
// -------------
useState('Mary')           // 1. Read the name state variable (argument is ignored)
useEffect(persistForm)     // 2. Replace the effect for persisting the form
useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle)     // 4. Replace the effect for updating the title


定義した順から処理されているのがわかるかと思う。
スキップは定義されていないので読み込まれるたびに処理が行われ都度レンダリングが行われる。
では、下記のブレークポイントを入れるとどうなるか?

// localStorageにnameプロパティの値が入っていない場合にsetItemを実行する

 if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

最初のレンダリングの際には条件を満たすのでFirst renderは変わらない。
ただし、Second renderはそうではなく以下の通りになる。

useState('Mary')           // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm)  //  This Hook was skipped!
useState('Poppins')        // 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle)     // 3 (but was 4). Fail to replace the effect

useState('Poppins')からエラーが出てしまっているのがわかる。
これはuseState Hookコールに対して返すべき値(useEffect(persistForm))がスキップされてしまい、未参照のまま処理が進んでしまったから。
よって、上記のブレークポイントは下記のようにuseEffect内に内包しないといけない。
これが冒頭の常にReact関数のトップレベルでHooksを使うということである。

// useEffect内にif文を定義する。
  useEffect(function persistForm() {
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

カスタムフックとHooks間での情報をパス、Reducerについて

これまで見てきたオンライン状態を管理する以下のようなコンポーネント。

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}


これを外部のコンポーネントでロジックとして呼び出し、処理上では以下の通りにしたい。


import React, { useState, useEffect } from 'react';

function FriendListItem(props) {

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

    // オンライン状態の場合は名前が緑色に、オフライン時は黒で表記される
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}


それではどうするのかというと

 const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });


この部分をカスタムフックとして共通化させることになる。
定義してみると以下のようになる。

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
    const[isOnline, setIsOnline] = useState(null);

    useEffect( () => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }

        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
    });

    return isOnline;
}


やりたいこととしてはisOnlineの状態を返したいということなので普通に関数を作ってやればいいということになるが、注意する点としては以下の3点である。

  • 通常のHooksと同じようにカスタムフックの最上位レベルで他のフックを呼び出すこと
  • 名前は必ずuseで始まるもので定義すること
  • 何を引数として取り、何を返すかは自分で定義すること

ではこのカスタムフックを実際に使ってみる。

import { useState, useEffect } from 'react';

function FriendStatus(props) {
    const isOnline = useFriendStatus(props.friend.id);

    if(isOnline === null) {
        return 'Loading....';
    }
    return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
    const isOnline = useFriendStatus(props.friend.id);

    return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

これまで見てきたようにHooksは定義した順に処理されているので処理としては

1 isOnlineがnullか否か返される
2 1で返ってきた結果によってisOnlineプロパティとして'Online'あるいは'Offline'のいずれかが値として返る。(初回render時は'Loading....'が返ってくる)
3 isOnlineがnullか否か返される
4 1で返ってきた結果によってstyleタグ内でカラープロパティの値が決まり、props.friend.nameがどの色で描画されるか決まる

という順番になる。
注意する点は2つのコンポーネントは共通しているので const isOnline = useFriendStatus(props.friend.id); は状態を共有しないということ。
つまり、FriendListItemで再度呼び出した段階でFriendStatusで呼び出したuseState及びuseEffectとは独立したものになっているということ。

では今度は作ったuseFriendStatusでHooks間で情報をパスするということをやってみる。

// 通常だとDBから引っ張ってくる部分
const friendList = [
    { id: 1, name: 'Phoebe' },
    { id: 2, name: 'Rachel' },
    { id: 3, name: 'Ross' },
];

function ChantRecipientPicker() {
    const [recipientID, setRecipientID] = useState(1);
    const isRecipientOnline = useFriendStatus(recipientID);

    return (
        <>
            <Circle color={isRecipientOnline ? 'green' : 'red'} />
            <select
              value={recipientID}
              onChange={e => setRecipientID(Number(e.target.value))}
            >
              {friendList.map(friend => (
                <option key={friend.id} value={friend.id}>
                  {friend.name}
                </option>
              ))}
            </select>
          </>
        );
}

処理順としては以下の通り

1 ChantRecipientPickerのuseStateによりrecipientIDに1がセットされる
2 const isRecipientOnlineにuseFriendStatusの結果を返す。このときrecipientIDを引数にセットする。
3 friendListの数だけが描画され、isRecipientOnlineの結果によってオンラインとオフライン時で{friend.name}の色が変わる

これによって選択したユーザーがオンライン状態であるか否かを表現することができる。

またReducerを使いたい場合は以下のようにする

// Reducer

function todosReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...state, {
                text : action.text,
                completed: false
            }];
    // ... other actions ...
    default:
      return state;
  }
}

// Hooks

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);

    function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
    }

    return [state, dispatch];
}


// ReducerでHooksを使いたい

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, []);

  function handleAddClick(text) {
    dispatch({ type: 'add', text });
  }

  // ...
}

処理順としては

1 Todosを実行。useReducerを実行する。
2 useReducerによって現在のstateとdispatchが返される(Constructorの役割)
3 Todosに戻り、handleAddClickによってdispatchが決定し、それに則ってtodosReducerが実行される(今回はcase 'add'の処理)。
4 todosReducerの返り値がtodosに保存される。

useReducerについて

useReducerについてもう少し、補足が欲しかったのでAPIを取得するケースを例に処理の流れを確認してみる。

import React, {useReducer, useEffect} from 'react';
import axios from 'axios';

const initialState = {
    isLoading: true,
    isError: '',
    post: {}
}

const dataFetchReducer = (dataState, action) => {
    switch(action.type) {
        case: 'FETCH_INIT':
        return {
            isLoading: true,
            post: {},
            isError: ''
        }
        case: 'FETCH_SUCCESS':
        return {
            isLoading: false,
            post: action.payload, // 後述のpayloadプロパティに入るdataを定義しておく
            isError: ''
        }
        case: 'FETCH_ERROR':
        return {
            isLoading: false,
            post: {},
            isError: 'Fetch is failure'
        }

        default:
            return datastate
    }

    const [dataState, dispatch] = useReducer(dataFetchReducer, initialState)

    useEffect(() =>
        axios
        .get() // 本来なら引数にURLリクエストURLを書くが省略

        .then(res =>{
            dispatch({type:'FETCH_SUCCESS', payload: res.data}) // 第2引数にデータを受け取るプロパティを設定する
        })
        .catch(err => {
            dispatch({type: 'FETCH_ERROR'})
        })
    )
}


処理順としては

1 useReducer実行、dataStateにinitialStateが代入される。
2 useEffect処理、axiosでリクエストを叩いて実行結果によってdispatchを選択し、dataFetchReducerを実行。
3 リクエスト成功ならcase: 'FETCH_SUCCESS'を実行、responseからpayloadにpost: action.payloadを代入。
4 リクエスト失敗ならcase: 'FETCH_ERROR'を実行。
5 dataStateが更新され、dataFetchReducerでreturnされたものがdataStateで管理される
→ postプロパティのtitleの値がほしければ、dataState.post.data、isLoadingプロパティの値がほしければdataState.isLoadingでアクセスできる。

最後に

やっぱりJavascript……ひいてはTypescriptはカロリーが高いなというものを感じました。
一応ここだけでも理解ができていればReact及びReduxのコードがだいぶわかるとは思うんですけども、Reduxの概念とかReactやReduxに付随するライブラリでHookを使うにはみたいなことも押さえておかないといけないので、フロントは結構根気がいるスキルなのだなと感じました。

とりあえず次はReact ReduxのHooksについてドキュメントを読んだあとReact Routerの同項を読んでReduxでReducerについて一度ちゃんと仕様を把握しようと思っています。
そこまで理解できればあとはAPIでログイン認証後にSessionIDを発行しそれを元にPermissionの制御とかSessionのCookieにユーザー情報入れたりでソーシャルログイン以外の基本の認証はどうにかなると踏んでいます……
今回、ここに行き着くまでにfirebase覚えればこのあたりすべてクリアじゃない? というアドバイスと知見も得たので独学だといずれ限界にくるレベルでやること、やらなきゃいけないことが増えていきますね……

あと、冒頭でも申し上げましたがエンジニアに就職しようとしています。
もしご興味持っていただけた方がいらっしゃいましたらTwitterのDMやリプライでお誘いいただけると幸いです……

Twitter
RESUME
Wantendly

参考

公式ドキュメント
React hooksを基礎から理解する (useReducer編)

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

ReactにSass(scss)を導入する

まえがき

今回は、ReactにSass(scss)を導入する方法を忘却録として記録します。
これから、ReactにSassを導入した方がいれば、参考にしれください。
では、初めて行きましょう。

ReactにSass(scss)を導入する

実装には、コマンド操作を行いますので準備をしてください。
私は、VScodeを使用します。

※また、Reactをインストールした初期画面を想定して設定します。

入力コマンド

コマンド
yarn add node-sass
npm install -s node sass
yarn add node-sass

以上3点を、共に実行しても良いですし、1つづつ実行しても問題ありません。

ファイルの拡張子の変更

コマンド
create-react-app Smple-Name

での生成時にできた
index.cssApp.css の拡張子を "css→scss" へ変更してください

importの修正

src/App.js
import React from 'react';
import logo from './logo.svg';
import './App.scss';  App.css  App.scss へ変更

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.scss';  index.scss  index.scss へ変更
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

ブラウザでの確認

以下のコマンドのどちらかを実行してください。
ブラウザで、問題なく Sass が読み込まれていればエラーが起こらないと思います。

コマンド
npm start
or
yarn start

http://localhost:3000/

Sassの変更

先ほど、ブラウザで確認しましたが、今回はSassを修正してみましょう。
初期設定の App.css (App.scss)を以下に修正します。

src/App.scss
.App {
  text-align: center;
  &-logo{
    height: 40vmin;
  pointer-events: none;
  }
  &-header{
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
  }
  &-link{
    color: #61dafb;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

変更をしたら、ブラウザで再び確認してください。

react-img.png

どうでしたか?うまくいきましたか?

以上がReactにSassを導入する方法でした。

あとがき

ここまで読んでいただき、ありがとうございました。
Sassを導入すると便利に編集ができるので、準備ができてよかったです。

参考リンク

React Starters: #4 Adding Sass to Create React App
参考動画様ありがとうございました。

Myリンク

また、Twitter・Portfolio のリンクがありますので、気になった方は
ぜひ繋がってください。プログラミング学習を共有できるフレンドが出来るととても嬉しいです。

Twitter
Portfolio
Github

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

ドロップダウンメニューを実装してeventオブジェクトに非同期でアクセスする際にエラーが出た。

今回、Reactでドロップダウンメニューを実装していて、詰まってしまった部分になります。

何が起こったか

824086e9e727e490924dcec88ceb2d42.gif

上記のように、ドロップダウンメニューを作成して、テストするか!というときにエラーが起きました。

1回目は選択できるのですが、2回目の選択でエラーになってしまう・・・。

書いたコード

今回、以下のような形で実装していました。

//ドロップダウンメニューの選択値によって値を変化させるためにuseStateを使う
//今回はドロップダウンメニューが複数あり、一つのuseState内でおさめたかったのでこんな感じで書いた
  const [selects, setSelects] = useState({
    name_id: 0,
    key_id: 0,
  })

//ドロップダウンメニュークリック時のイベントオプションを定義
  const handleDropdownChange = (event: any) => {
    const name = event.target.name
    setSelects(() => {
      return { ...selects, [name]: event.target.value }
    })
  }

//今回は配列からドロップダウンメニューを作成したかったので以下のように書く
//key_idも同じ用に書いてるので今回は省略
  List = names.map((a, index) => (
    <option value={`${a.name_id}`} key={index}>
      {a.name_id}
    </option>
  ))

//表示したい部分で下記のようにして表示
  <select name="name_id" onChange={handleDropdownChange}>
    {List}
  </select>

解決策

公式に書いてました。

参照できない理由

The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.

つまり、パフォーマンスの向上のために一度読み込まれたイベントオブジェクトは再利用されて、すべてのプロパティがnull化されてしまうので、非同期的にイベントにアクセスすることはできないということですね。

私が上記で一回目は問題なくドロップダウンメニューで選択できていたのは、イベントコールバックが一回目の呼び出しだったからで、二回目以降は最初に呼び出されたイベントオブジェクトが再利用されてしまうので、そのときに違う値が選択されているとエラーが発生する(=非同期的にアクセスできない)という理由だったみたいです。

というわけで解決策。解決策も公式に書いてました。

Note:
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.

つまり、非同期でアクセスさせるためにはevent.persist()を使ってくれということです。

解決

//ドロップダウンメニュークリック時のイベントオプションを定義
  const handleDropdownChange = (event: any) => {
    event.persist(); //これを追加
    const name = event.target.name
    setSelects(() => {
      return { ...selects, [name]: event.target.value }
    })
  }

上記で解決!

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

今更きく…Vue3とは?

まえがき

最近、社内でVue2で作られているアプリケーションのUIを刷新する計画をしており、Vue3を導入するかの議論がされました。
2018年後半からVue3の開発が開始されてから約二年。
Vue3もRCとなり、リリースに向けて動きはじめました。
Vue3のComposition API等具体的な実装部分に触れている記事はよくみますが
大枠に触れている記事が少ないので書いてみます。

EvanYou氏がある記事の中で、「なぜ書き換えたか?」について語っているので、私なりにまとめました。

なぜ書き換えたか?

新しい言語機能の活用

ES2015の最新版への対応が各ブラウザで行われており、Vueも対応する必要がありました
その中でも、Proxyが一番注目しており、VueでもProxyを活用することで、パフォーマンス改善を行うことができます。

アーキテクチャの問題へ対応

Vueではコードが暗黙的な結合という形で、技術負債を積み上げてきました。
それにより、コントリビューターが変更を加えることが困難になっていました。
これらを解決し、コードを変更をしやすくする必要がありました。

改善点

Typescriptのサポート

Vue2はもともとプレーンESで作成されていましたが、TypeScriptをサポートしました。

内部パケージの分離

monorepo(一つのリポジトリでパッケージを管理すること)で、内部パッケージ化を行い、それぞれが独自のAPI、タイプ定義、およびテストを実装しています。
それにより、モジュール間の依存関係をより明確にし、開発者がすべてを読み、理解し、変更しやすくし、プロジェクトの貢献の障壁を下げ、長期的な保守性を改善しました。

RFCプロセスの設定

ユーザーが重大な変更についてフィードバックを提供できるよう、RFC(Request for Comments)プロセスを採用しました。
議論はGitHubリポジトリで行われ、提案はプルリクエストとして送信されるため、コメントで有機的に議論が展開されます。

Vueはもともと、軽量のフロントエンドフレームワークですが、更に軽量化を行っています。

仮想DOMのボトルネックの改善

Vue 3では、適切なAST変換パイプラインを使用してコンパイラーを書き直しました。
これにより、コンパイル時の最適化を行っています。

  • ブロック内のノード更新の際、ツリーのトラバースの最適化を行いました。
    • この最適化は、実行する必要のあるツリートラバーサルの量を1桁減らすことで、仮想DOMのオーバーヘッドの多くを回避します。
  • メモリ使用率軽減が大幅に向上し、ガベージコレクションの頻度が減少します。
  • 要素レベルでは、コンパイル段階で実行計画の作成を行い、ランタイムがそれをヒントに実行を行うことで高速化を実現しています。

これらの手法を組み合わせると、レンダリング更新のベンチマークが大幅に改善されています。
Vue3がVue2のCPU時間(Javascript計算の実行に費やされた時間)の10分の1未満になることもあります。

バンドルサイズの最小化

フロントエンドフレームワークは、サイズそのものがパフォーマンスに影響しています。
Vueはもともと軽量(圧縮しても約23KB)でしたが、2つの問題に気がつきました。

  • 利用していない機能まで、ダウンロードと解析のコストが発生している。
  • 機能を追加するにつれ、容量は無限に増え続ける。

この問題を解決するにはツリーシェイキングを行い、不要なコードを削除することでした。
Vue3では、ほとんどのグローバルなAPI、内部ヘルパーをESモジュールにすることで、これを実現しました。
多くの新機能の追加にも関わらず、Vue2の半分以下の容量を実現しています。

まとめ

ここでは触れていませんが多くの機能が追加されています。

  • Composition APIの導入
  • multi-v-model機能追加
  • Teleport機能追加
  • Fragment機能追加
  • Suspense機能追加
  • フィルターの廃止

等々…

上記に加え、パフォーマンスの向上を考えると、導入しない手はないかなと個人的に考えており
下位互換性もあるとのことなので、正式版がでたら対応を考えたいと思います。
Composition APIの導入で、少しReact側によったことからReactでもいいのでは?という賛否両論の意見が出ていますが
軽量や学習コストの低さという部分では、Vueが依然として変わらないと思うので、棲み分けとしては十分できているのかと思ってたりします。

今回は、ブログをもとに私が解釈した内容で書いているので、認識の齟齬等あれば教えて頂きたいです。

元ネタ:https://increment.com/frontend/making-vue-3/?ref=madewithvuejs.com#why-rewrite

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

React + TypeScript環境構築

React&TypeScriptの環境を2発で作るコマンドがあったのでメモ。

作り方

yarn create react-app react-ts-project --template typescript
yarn add typescript @types/node @types/react @types/react-dom @types/jest

なんとなくドキュメントを読んでたら見つけた!

https://create-react-app.dev/docs/adding-typescript/

いままでの作り方

↓を参考に作った。

https://qiita.com/yukitaka13-1110/items/f8b4ed8eff8f5ad36476

ざっき

よくやる設定とか、グローバルなstoreの設定とか認証の実装とか、何回も書くのが面倒なのでテンプレ化したい。

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

Recoilでasync selectorできないメモ

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

Mac環境でnpm startでエラー。

create-react-app アプリ名

でアプリを作り、いざ起動。

npm start

あれ、実行できない??

以下エラー文。

Starting the development server...

dyld: lazy symbol binding failed: Symbol not found: _FSEventStreamCreate
  Referenced from: /Users/user/Documents/cycle/node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node
  Expected in: flat namespace

dyld: Symbol not found: _FSEventStreamCreate
  Referenced from: /Users/user/Documents/cycle/node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node
  Expected in: flat namespace

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! cycle@0.1.0 start: `react-scripts start`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the cycle@0.1.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/user/.npm/_logs/2020-09-10T15_49_19_237Z-debug.log

まずはいかに注目。

npm ERR! code ELIFECYCLE
npm ERR! errno 1
(以下略)

Google検索にエラー文を貼り付けてみたが、解決する方法が見つからない。。。。

次にいかに注目して検索。。。

dyld: lazy symbol binding failed: Symbol not found: _FSEventStreamCreate
  Referenced from: /Users/user/Documents/cycle/node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node
  Expected in: flat namespace

dyld: Symbol not found: _FSEventStreamCreate
  Referenced from: /Users/user/Documents/cycle/node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node
  Expected in: flat namespace

解決できそうな記事を発見!!

でも、

記事のエラー文
  Referenced from: /Users/bunnyxt/Projects/njauiot-frontend/node_modules/fsevents/build/Release/fse.node

実際のエラー文
 Referenced from: /Users/user/Documents/cycle/node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node

と微妙に異なっていて、うまく解決しない。。。

うーん困った。。。。

node_modules/webpack-dev-server/node_modules/fsevents/build/Release/fse.node

いっそfsevents以下のディレクトリを削除すればいいのでは? との記事を発見。

実行。

解決!!

ついでに、HOSTが固定されていたので

unset HOST

これでサーバーの指定を解除。

この状態で再度

npm start

を実行。

プレビューがうまく表示されました!!!

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

新サービスZennが面白い!

Zennとは

プログラマーのための新しい情報共有コミュニティサービスです。
知見を記事にして公開したり、より深い知見は本として有料(無料)で公開することのできるサービスです。

情報を発信するプログラマーが対価を得られるようにというのが、このサービスを作られた方の思いなのだと思います。

はじめ方1.png

Zennってどんな感じ?

ログインはGoogleアカウントのみ!

ログインについては、Googleアカウントでのログインのみのようです。(シンプルで良いね?)
はじめ方2.png

書ける記事は2種類!

Add Newをクリックすると、記事(Article)と本(Book)の2種類から書きたい方を選べます。
記事執筆1.png

記事はマークダウンで書ける!

プログラマーには嬉しいマークダウンで記事が書けちゃいます。
記事執筆3.png

プレビュー機能!

▷ボタンをクリックするとマークダウンで書いたものをプレビュー出来ます。
記事執筆4.png

豊富な挿入機能!

Twiiter、CodePen、JSFiddle、YouTube、SlideShare、SpeakerDeckと記事執筆に役立つ他サービスを埋め込み出来ます。
記事執筆6.png

記事の設定はシンプル!

アイコンは好きなものに変えられます。カテゴリーはテック系とアイデア系。
トピックスはQiitaを活用している皆さんならよくご存知のやつですね?
記事執筆5.png

本はチャプター式!

本の書き方としては、記事(Article)の書き方と変わらない感じで、記事をいくつか書いたのを合わせていく感じです。
本執筆2.png

本のプレビューはこんな感じ!

本のプレビューはPCだとこんな感じです。Teckpitのような感じですね?
本執筆5.png

これは非常に面白いサービスだと思った方も多いのではないでしょうか?(^^)
僕もさっそく使ってみました。本も執筆しようかなと思ってます。良かったらチェックしてね?
https://zenn.dev/engineerhikaru

※この記事は勝手に書いたもので、決して何かをもらっているから書いているものではございません笑

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

Material-UIのチェックボックスを使う際はdata属性に注意

はじめに

Reactの人気UIライブラリであるMaterial-UIにおいて、
チェックボックスのonChange時の処理を書いていたらハマった点がありました。

作ろうとしていたもの

テーブルの行ごとにチェックボックスが付いていてクリックすると削除等の処理を行えるものです。
要は各IDを持つリスト型のデータがあり、それに紐づくチェックボックスがあるUIになります。
ですが、このIDの取得ができませんでした。

テーブルのコードを書くのは煩雑なので、要点を絞って簡略化した例で説明します。
注:TypeScriptです。

失敗例

チェックボックスUIにdata属性で各アイテムのidを紐付け、
それをコンソールに表示させようとしています。

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

さて結果は...

スクリーンショット 2020-09-17 2.47.34.png

期待外れでした。

ちなみに、チェックボックスUIを<input type="checkbox"...のように素のJSXで表現すれば正しくIDは表示されます。

ここでマークアップ構造を確認すると...

スクリーンショット 2020-09-17 2.50.03.png

data属性はinput要素から沢山離れてしまっています。
落ち着いて考えればあれだけリッチなUIを作るためにはこれくらいマークアップが覆われているのはすぐ想像できるかもしれません。

ということで、data属性にidを紐付ける方法をやめました。

成功例

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => {
        const id = item.id;
        return (
          <li key={id}>
            <Checkbox onChange={() => handleChange(id)} />
          </li>
        );
      })}
    </ul>
  );
}

これを同様にコンソールで確認してみると...

スクリーンショット 2020-09-17 2.54.58.png

正常に取得できています。
これで一件落着ですね。

一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。

確認用のソースコード全体

App.tsx
import React from 'react';
import Checkbox from '@material-ui/core/Checkbox';
import './App.css';

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => {
        const id = item.id;
        return (
          <li key={id}>
            <Checkbox onChange={() => handleChange(id)} />
          </li>
        );
      })}
    </ul>
  );
}

function App() {
  return (
    <div>
      <Checkboxs1 />
      <Checkboxs2 />
    </div>
  );
}

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

UIライブラリを使う際の落とし穴

はじめに

Reactの人気UIライブラリであるMaterial-UIにおいて、
チェックボックスのonChange時の処理を書いていたらハマった点がありました。

作ろうとしていたもの

テーブルの行ごとにチェックボックスが付いていてクリックすると削除等の処理を行えるものです。
要は各IDを持つリスト型のデータがあり、それに紐づくチェックボックスがあるUIになります。
ですが、このIDの取得ができませんでした。

テーブルのコードを書くのは煩雑なので、要点を絞って簡略化した例で説明します。
注:TypeScriptです。

失敗例

チェックボックスUIにdata属性で各アイテムのidを紐付け、
それをコンソールに表示させようとしています。

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

さて結果は...

スクリーンショット 2020-09-17 2.47.34.png

期待外れでした。

ちなみに、チェックボックスUIを<input type="checkbox"...のように素のJSXで表現すれば正しくIDは表示されます。

ここでマークアップ構造を確認すると...

スクリーンショット 2020-09-17 2.50.03.png

data属性はinput要素から沢山離れてしまっています。
落ち着いて考えればあれだけリッチなUIを作るためにはこれくらいマークアップが覆われているのはすぐ想像できるかもしれません。

ということで、data属性にidを紐付ける方法をやめました。

成功例

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox onChange={() => handleChange(item.id)} />
        </li>
      ))}
    </ul>
  );
}

これを同様にコンソールで確認してみると...

スクリーンショット 2020-09-17 2.54.58.png

正常に取得できています。
これで一件落着ですね。

一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。

教訓

HTMLの確認を怠らないようにしましょう。

更新履歴

2020/09/17 ソースコードを微調整しました

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