20201125のReactに関する記事は17件です。

reactで別のコンポーネントを引数を渡しつつ呼び出す場合

呼び出す側

<コンポーネント名 props名="値"/>の要領で書く

<td><GetPod name="vamdemic111aaa-app"/></td>

呼び出される側

  • propsに入る
  • this.props.nameで使うことができる
class GetPod extends Component {
    constructor(props) {
        super(props);
        this.state = {
            podstatus: ''
        }
    }

    // renderの前に実行される
    componentWillMount() {
    const json = {"Name":this.props.name};
    const convert_json = JSON.stringify(json);
    const obj = JSON.parse(convert_json);
    console.log(obj)
    console.log(this.props.name)
    const request = axios.create({
        baseURL: "http://127.0.0.1:1323",
        headers: {
            'Content-Type': 'application/json',
        },
        responseType: 'json',
        method: "GET"
   })

   request.get("/getpodstatus", obj)
        .then(request => {
           this.setState({
               // podstatus: request.data.items[0].status.phase
               podstatus: request.data.items[0].status.phase
           })
           console.log(this.state.podstatus)
       })
    }

    // コンポーネントが呼ばれたらstateを返すようにする
    render() {
        return <p>{this.state.podstatus}</p> ;
    }
}

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

TypeScript/JavaScriptの諸々の演算子達(!, ?, &, =)の役割をまとめてみた(初心者向け)

React(TypeScript)、「!」とか「?」とか「&」とか記号多すぎて訳わからんのだが!!
というところからこの記事を書くに至りました。

記号ってただでさえググりづらいし、TS/JS関連だとOptional Chainingとか、Nullish Coalescingとか名前もいちいち覚えづらい…
ので自分用のメモとしても残しておきたいな、と。

記事の内容としては、下記の通り演算子周りが主となります。
• 短絡評価 (||演算子と&&演算子)
• &&演算子
• !(非nullアサーション演算子)
• ?. (Optional Chaining)
• == と=== (等価演算子)
• 三項演算子

初心者ゆえにざっくりとした理解に重きを置いてますので悪しからず。。

短絡評価 (ショートサーキット評価) ー ||演算子と&&演算子 ー

AND演算子(&&)、OR演算子(||)自体はどの言語でもあるかと思いますが、
Reactだとこの短絡評価をよく見かけます。(で、これが正直読みづらい…)
ナウい書き方らしいので頑張りましょう。

短絡評価(ショートサーキット評価)とは
左辺を評価した時点で結果が確定した時は、右辺が評価されないという特性のこと。
以下で||演算子と、 &&演算子でそれぞれ見てみる。

「||演算子 (OR)」 の時

論理演算子||の左側がtrueであれば左側の値、そうでなけれな右側を返す。
(反対に、左側がfalseなら右側を返す)

簡潔に書くと
左辺がtrue/truthyなら左辺を返す
左辺がfalse/falsyなら右辺を返す
(左辺がfalseだったら、右辺も見にいくみたいな感覚でいます。)

ややこしいのが、ショートサーキット評価では、bool値以外の値(普通の文字列とか数値とか)である場合は、暗黙に型変換して評価している、ということ。
(値がtruthy/falsyか、という評価観点)

例えば…数値だと0がfalsy、文字列だと””空文字がfalsyとして評価されます。
つまり、 falsy として定義された値 (つまり、false, 0, -0, 0n, “”, null, undefined, NaN)はfalse判定、それ以外は全てture判定。

実際の処理を見るとなんとなく分かってくる…かもしれません。

10 || false;       // 10
10 || true;        // 10
10 || 20;          // 10
false || 20;       // 20
0 || "zero";       // "zero" 「0」はfalsyなので、"zero"を返す

"Cat" || "Dog";    // "Cat" 
"Cat" || false;    // "Cat"
false || "Cat";    // "Cat" 
"" || "Cat";       // "Cat" 「""」はfalsyなので、"Cat"を返す

なるほど、だんだん分かってきた気がする。

これを活用すると、条件分岐のif/elseを省略して書くことができて、実務だと下記のような使われ方をしてます。
(後ほど書く??演算子も同様)

// 変数 user の値がtruthyならname, そうでなければ文字列'名無し'が変数userにセットされる

// 条件分岐省略前
let user;

if (user) {
   user = name;
} else{
   user = '名無し';
}

// 条件分岐省略後
let user = user || '名無し';   //userが存在すればuserを、なければ'名無し'を代入

・参考
Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
JavaScript 論理演算子OR「||」の特殊な用法 | プログラミングアカデミー ゼロプラスワン

「&&演算子 (AND)」 の時 

A AND Bという論理式があったとして、
Aがfalseの時点で式全体の結果はfalseで確定するので、Bがどうであるかについてはチェックしない
というのがショートサーキット評価の考え方です。

簡潔に書くと、
左辺がtrue/truthyなら右辺を返す
左辺がfalse/falsyなら左辺を返す
(左辺がtrueだったら、右辺に行ける、みたいな感覚ですね。)

true  && true;     // true 
true  && false;    // false
false && true;     // false
"Cat" && "Dog";    // "Dog" "Cat"はtrutyだから左辺を返す
false && "Cat";    // false
"Cat" && false;    // false
"" && "Cat";       // ""    「""(空文字)」はfalsyだから左辺を返す
0 && 10;           // 0     「0」はfalsyだから左辺を返す

Reactでは結構下記のような感じで使われてます。

list && list.forEach(item) => {処理}

listがfalsyだったらReactは描画せず、trutyだったら処理を行って描画する、といった感じでしょうか。
可読性の観点で下記記事では見事にクソコードと罵られていますが、、笑

JavaScript 論理演算子AND「&&」の特殊な用法(Reactでよく見る記法) | プログラミングアカデミー ゼロプラスワン

||演算子と&&演算子まとめ

とりあえず、
 ||演算子は左辺がfalseだったら右辺を見に行く(返す)!
 &&演算子は左辺がtrueだったら右辺を見に行く(返す)!
というのが重要になりそうです。

・参考
JavaScriptの「&&」「||」について盛大に勘違いをしていた件 - Qiita

「??」演算子 ー Null合体演算子 / Nullish Coalescing ー

左辺が 「nullishな値の時だけ」、右辺が評価される 。
(上記のOR演算子|| と似てるけどちょっと違う。)

違いを明確に…
•  ??演算子:左辺がnullish(=nullまたはundefined)のときだけ、右辺が評価
•  || 演算子:左辺がfalsy(false, 0, -0, 0n, “”, null, undefined, NaN)のときに、右辺が評価

console.log(0 || 1); // 1
console.log(0 ?? 1); // 0

console.log('' || '空文字'); // 空文字
console.log('' ?? '空文字'); // (何も表示されない)

要は、??演算子の方がより厳密に undefined や null を判定してくれるものと覚えておけば良さそう。

「?.」でアクセス ー Optional Chaining ー

obj?.foo

Rubyのぼっち演算子的な役割。(やったぜ)
プロパティのアクセス先が「nullishな値(null または undefined)」の場合、
通常の.でアクセスすると落ちるが、?.だとundefinedが返るので回避できる。

?.でプロパティアクセスしていって、上記の??演算子と組み合わせると、nullishだったら、〜〜(処理)みたいに書けて便利らしい。

・参考
そろそろJavaScriptに採用されそうなOptional Chainingを今さら徹底解説 - Qiita

プロパティ/変数アクセスの「!」 ー 非nullアサーション演算子/ Non-null assertion operator ー

item.color!.toUpperCase()

ここでのcolor!みたいなやつです。

!をつけることで、対象プロパティ/変数がnon-nullであるということをコンパイラに明示するもの。
…要は、『こいつは絶対に null も undefined も入らないよ!』とコンパイラを強引に黙らせ、コンパイルエラーを回避します。
最初これをぼっち演算子(&.)と似たものと思ったのですが、これはあくまでコンパイラを黙らせるだけで、実際にNullがきたら落ちるので注意。
(ちなみに『りあクト!』では極力使うな、と書いてありましたが、現PJでは結構見かける。。)

・参考
TypeScriptのコードレビューを依頼された人のための!と?の解説 | Developers.IO
strictNullChecks - TypeScript Deep Dive 日本語版

条件演算子 (三項演算子)

If~else文の省略バージョン。
普通にRubyでも書けるけど、なぜかRailsのPJより多く使ってる印象。

// 変数 = 条件式 ? trueの時の値/処理 : falseの時の値/処理;
let age = 28
let value = (age >= 30) ? "over thirty" : "under thirty"

「==」 と 「===」 ー 等価演算子と厳密等価演算子 ー

「==」 等価演算子

ご存知の通り、「A == B」でAとBが等しいかどうか、という意味ですが、
等価演算子では暗黙な型変換が行われます。

1 == "1"            // true 文字列を数値に変換後、比較
true == 1           // true 真偽値を数値に変換後、比較
"" == 0             // true 文字列を数値に変換後、比較
null == undefined   // true

等価演算子は両辺の型を解釈し、異なる型の場合に同じ型に変換をしてくれる

「===」 厳密等価演算子

そして厳密等価演算子はと言うと、型も含めて厳密に比較します。

1 == "1"            // false 
true == 1           // false 
"" == 0             // false
null == undefined   // false

===のほうが厳密に比較してくれるってことですね。

if(hoge)

記事に載せるかちょっと迷ったものの、これもtruthy/falsy絡みでもあるので。

if(hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)以外の時は、true判定
  要はちゃんと値があればtrue判定

if(!hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)の時はtrue判定

「|」 (パイプライン一本) ー ユニオン型 ー

これはユニオン型(union type)を定義するときに使うもの。
| で並べることで、複合的な型を定義できる。

// idは数値でも文字列でもOK
let id: number | string
// nullも許すstring型
type nullableString = string | null;

TypeScriptのはなし : ユニオン型がけっこうイイね - 檜山正幸のキマイラ飼育記 (はてなBlog)

以上です。
あくまで初心者による初心者向けの内容なので、より深い知識をお求めの際は参考記事等々読んでくださいませ。
そもそも理解間違ってる、という部分あればなんなりとご指摘ください!

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

【初心者向け】TypeScript/JavaScriptで頻出する演算子(!, ?, &, =)の用法をまとめてみた

React(TypeScript)、「!」とか「?」とか「&」とか記号多すぎて訳わからんのだが!!
というところからこの記事を書くに至りました。

記号ってただでさえググりづらいし、TS/JS関連だとOptional Chainingとか、Nullish Coalescingとか名前もいちいち覚えづらい…
ので自分用のメモとしても残しておきたいな、と。

  • 対象読者

    • ReactとかTypeScript/JavaScriptを触り始めた人
    • 業務で上記言語のソースコード読むのに苦労してる人
    • いちいち演算子/記号ググるのだるい人
  • 記事の内容

    • ||演算子と&&演算子の短絡評価
    • &&演算子
    • !(非nullアサーション演算子)
    • ?. (Optional Chaining)
    • 三項演算子
    • == と=== (等価演算子)
      etc…

初心者ゆえにざっくりとした理解に重きを置いてますので悪しからず。。

1. ||演算子と&&演算子の短絡評価 (ショートサーキット評価)

AND演算子(&&)、OR演算子(||)自体はどの言語でもあるかと思いますが、
Reactだとこれらの短絡評価をよく見かけます。(で、これが正直読みづらい…)
ナウい書き方らしいので頑張りましょう。

短絡評価(ショートサーキット評価)とは
左辺を評価した時点で結果が確定した時は、右辺が評価されないという特性のこと。
以下で||演算子と、 &&演算子でそれぞれ見てみる。

① ||演算子 (OR) の場合

論理演算子||の左側がtrueであれば左側の値、そうでなけれな右側を返す。
(反対に、左側がfalseなら右側を返す)

簡潔に書くと
左辺がtrue/truthyなら左辺を返す
左辺がfalse/falsyなら右辺を返す
(左辺がfalseだったら、右辺も見にいくみたいな感覚でいます。)

ややこしいのが、ショートサーキット評価では、bool値以外の値(普通の文字列とか数値とか)である場合は、暗黙に型変換して評価している、ということ。
(値がtruthy/falsyか、という評価観点)

例えば…数値だと0がfalsy、文字列だと””空文字がfalsyとして評価されます。
つまり、 falsy として定義された値 (つまり、false, 0, -0, 0n, “”, null, undefined, NaN)はfalse判定、それ以外は全てture判定。

実際の処理を見るとなんとなく分かってくる…かもしれません。

10 || false;       // 10
10 || true;        // 10
10 || 20;          // 10
false || 20;       // 20
0 || "zero";       // "zero" 「0」はfalsyなので、"zero"を返す

"Cat" || "Dog";    // "Cat" 
"Cat" || false;    // "Cat"
false || "Cat";    // "Cat" 
"" || "Cat";       // "Cat" 「""」はfalsyなので、"Cat"を返す

なるほど、だんだん分かってきた気がする。

これを活用すると、条件分岐のif/elseを省略して書くことができて、実務だと下記のような使われ方をしてます。
(後ほど書く??演算子も同様)

// 変数 user の値がtruthyならname, そうでなければ文字列'名無し'が変数userにセットされる

// 条件分岐省略前
let user;

if (user) {
   user = name;
} else{
   user = '名無し';
}

// 条件分岐省略後
let user = user || '名無し';   //userが存在すればuserを、なければ'名無し'を代入

・参考
Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
JavaScript 論理演算子OR「||」の特殊な用法 | プログラミングアカデミー ゼロプラスワン

② &&演算子 (AND) の場合

A AND Bという論理式があったとして、
Aがfalseの時点で式全体の結果はfalseで確定するので、Bがどうであるかについてはチェックしない
というのがショートサーキット評価の考え方です。

簡潔に書くと、
左辺がtrue/truthyなら右辺を返す
左辺がfalse/falsyなら左辺を返す
(左辺がtrueだったら、右辺に行ける、みたいな感覚ですね。)

true  && true;     // true 
true  && false;    // false
false && true;     // false
"Cat" && "Dog";    // "Dog" "Cat"はtrutyだから左辺を返す
false && "Cat";    // false
"Cat" && false;    // false
"" && "Cat";       // ""    「""(空文字)」はfalsyだから左辺を返す
0 && 10;           // 0     「0」はfalsyだから左辺を返す

Reactでは結構下記のような感じで使われてます。

list && list.forEach(item) => {処理}

listがfalsyだったらReactは描画せず、trutyだったら処理を行って描画する、といった感じでしょうか。
可読性の観点で下記記事では見事にクソコードと罵られていますが、、笑

JavaScript 論理演算子AND「&&」の特殊な用法(Reactでよく見る記法) | プログラミングアカデミー ゼロプラスワン

||演算子と&&演算子まとめ

とりあえず、
 ||演算子は左辺がfalseだったら右辺を見に行く(返す)!
 &&演算子は左辺がtrueだったら右辺を見に行く(返す)!
というのが重要になりそうです。

・参考
JavaScriptの「&&」「||」について盛大に勘違いをしていた件 - Qiita

2. ??演算子 ~ Null合体演算子 / Nullish Coalescing ~

左辺が 「nullishな値の時だけ」、右辺が評価される 。
(上記のOR演算子|| と似てるけどちょっと違う。)

違いを明確に…
•  ??演算子:左辺がnullish(=nullまたはundefined)のときだけ、右辺が評価
•  || 演算子:左辺がfalsy(false, 0, -0, 0n, “”, null, undefined, NaN)のときに、右辺が評価

console.log(0 || 1); // 1
console.log(0 ?? 1); // 0

console.log('' || '空文字'); // 空文字
console.log('' ?? '空文字'); // (何も表示されない)

要は、??演算子の方がより厳密に undefined や null を判定してくれるものと覚えておけば良さそう。

3. 「?.」でアクセス ~ Optional Chaining ~

obj?.foo

Rubyのぼっち演算子的な役割。(やったぜ)
プロパティのアクセス先が「nullishな値(null または undefined)」の場合、
通常の.でアクセスすると落ちるが、?.だとundefinedが返るので回避できる。

?.でプロパティアクセスしていって、上記の??演算子と組み合わせると、nullishだったら、〜〜(処理)みたいに書けて便利らしい。

・参考
そろそろJavaScriptに採用されそうなOptional Chainingを今さら徹底解説 - Qiita

4. プロパティ/変数アクセスの「!」 ~ 非nullアサーション演算子/ Non-null assertion operator ~

item.color!.toUpperCase()

ここでのcolor!みたいなやつです。

!をつけることで、対象プロパティ/変数がnon-nullであるということをコンパイラに明示するもの。
…要は、『こいつは絶対に null も undefined も入らないよ!』とコンパイラを強引に黙らせ、コンパイルエラーを回避します。
最初これをぼっち演算子(&.)と似たものと思ったのですが、これはあくまでコンパイラを黙らせるだけで、実際にNullがきたら落ちるので注意。
(ちなみに『りあクト!』では極力使うな、と書いてありましたが、現PJでは結構見かける。。)

・参考
TypeScriptのコードレビューを依頼された人のための!と?の解説 | Developers.IO
strictNullChecks - TypeScript Deep Dive 日本語版

5. 条件演算子 (三項演算子)

If~else文の省略バージョン。
普通にRubyでも書けるけど、なぜかRailsのPJより多く使ってる印象。

// 変数 = 条件式 ? trueの時の値/処理 : falseの時の値/処理;
let age = 28
let value = (age >= 30) ? "over thirty" : "under thirty"

6. 「==」 と 「===」 ~ 等価演算子と厳密等価演算子 ~

① 「==」 等価演算子

ご存知の通り、「A == B」でAとBが等しいかどうか、という意味ですが、
等価演算子では暗黙な型変換が行われます。

1 == "1"            // true 文字列を数値に変換後、比較
true == 1           // true 真偽値を数値に変換後、比較
"" == 0             // true 文字列を数値に変換後、比較
null == undefined   // true

等価演算子は両辺の型を解釈し、異なる型の場合に同じ型に変換をしてくれる

② 「===」 厳密等価演算子

そして厳密等価演算子はと言うと、型も含めて厳密に比較します。

1 == "1"            // false 
true == 1           // false 
"" == 0             // false
null == undefined   // false

===のほうが厳密に比較してくれるってことですね。

7. if(hoge) ~ 比較演算子の無いif文 ~

if(hoge) {処理}という具合で、条件式が無いif文を結構見かけて、最初驚きました。
これ自体は記事に載せるかちょっと迷ったものの、これもtruthy/falsy絡みでもあるので。

if(hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)以外の時は、true判定
  要はちゃんと値があればtrue判定

if(!hoge)のとき
→ hogeがfalsyな値(null, undefined, 0, 空文字(''), false)の時はtrue判定

8. 「|」 (パイプライン一本) ~ ユニオン型 ~

これはユニオン型(union type)を定義するときに使うもの。
| で並べることで、複合的な型を定義できる。

// idは数値でも文字列でもOK
let id: number | string
// nullも許すstring型
type nullableString = string | null;

TypeScriptのはなし : ユニオン型がけっこうイイね - 檜山正幸のキマイラ飼育記 (はてなBlog)

以上です。
あくまで初心者による初心者向けの内容なので、より深い知識をお求めの際は参考記事等々読んでくださいませ。
そもそも理解間違ってる、という部分あればなんなりとご指摘ください!

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

Javascript(React) 株価取得WebAPI グラフ表示(Function Component)

FireShot Capture 008 - React App - localhost.png

API-GRAPH-REACTという名前で
githubにて公開中です。

検索欄入力の際チィッカーシンボルを入力してください
例:
Amazon => AMZN
Microsofl => MSFT
Apple => APPL
Facebook => FB

#連想配列

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

reactでstateを別コンポーネントに渡すときの呼び出される側

呼ばれる側

  • コンポーネントが呼ばれて返す値にstateをセットすると別コンポーネントから呼び出すことができる
class GetPod extends Component {
    constructor(props) {
        super(props);
        this.state = {
            podstatus: ''
        }
    }

    // renderの前に実行される
    componentWillMount() {
    const request = axios.create({
            baseURL: "http://127.0.0.1:1323",
            method: "GET"
        })
        request.get("/getpodstatus")
            .then(request => {
                this.setState({
                    podstatus: request.data.items[0].status.phase
                })
                console.log(this.state.podstatus)
            })
    }

    // コンポーネントが呼ばれたらstateを返すようにする
    render() {
        return <p>{this.state.podstatus}</p> ;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[react] 多次元配列をテーブルに挿入するfunction component

オライリージャパンreatビギナーズガイドの練習課題『高機能excel作成を作ろう』にて
多次元配列の処理をclass componentで書かれていた。
function componentの理解のため、function componentに書き換えていく。

参考書籍:reactビギナーズガイド
サンプルコード:https://github.com/stoyan/reactbook
該当フォルダ・ファイル:chapters/03.04.table-sortby.html

FireShot Capture 003 - React App - localhost.png
配列の要素は適当に違う値を使用します。

Table.jsx
function Table() {

  const heads = ["", "", "", "", ""]
  const heads1 = heads.map((head) =>
    <th>{head}</th>
  )

  const data = [
           [1, 2, 3, 4, 5], 
           [6, 7, 8, 9, 10,], 
           [11, 12, 13, 14, 15], 
           [16, 17, 18, 19, 20,], 
           [21, 22, 23, 24, 25], 
           [26, 27, 28, 29, 30,]
               ]



  return (
    <div>
      <table border='1' cellSpacing='0'>
        <tr>{heads1}</tr>
        {data.map(row1 =>
          <tr>
            {row1.map(row2 =>
              <td>{row2}</td>
            )}
          </tr>
        )}
      </table>
    </div>
  );
}

export default Table;

(随時更新予定)

#react
#ファンクションコンポーネント
#Table
#多次元配列
#二次元配列
#map関数

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

Javascript(react) 多次元配列をテーブルに挿入するfunction component

オライリージャパンreatビギナーズガイドの練習課題『高機能excel作成を作ろう』にて
多次元配列の処理をclass componentで書かれていた。
function componentの理解のため、function componentに書き換えていく。

参考書籍:reactビギナーズガイド
サンプルコード:https://github.com/stoyan/reactbook
該当フォルダ・ファイル:chapters/03.04.table-sortby.html

FireShot Capture 003 - React App - localhost.png
配列の要素は適当に違う値を使用します。

土日がない?休みなんか必要ないですよね?笑

Table.jsx
function Table() {

  const heads = ["", "", "", "", ""]
  const heads1 = heads.map((head) =>
    <th>{head}</th>
  )

  const data = [
           [1, 2, 3, 4, 5], 
           [6, 7, 8, 9, 10,], 
           [11, 12, 13, 14, 15], 
           [16, 17, 18, 19, 20,], 
           [21, 22, 23, 24, 25], 
           [26, 27, 28, 29, 30,]
               ]



  return (
    <div>
      <table border='1' cellSpacing='0'>
        <tr>{heads1}</tr>
        {data.map(row1 =>
          <tr>
            {row1.map(row2 =>
              <td>{row2}</td>
            )}
          </tr>
        )}
      </table>
    </div>
  );
}

export default Table;

(随時更新予定)

#react
#ファンクションコンポーネント
#Table
#多次元配列
#二次元配列
#map関数

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

Atomic Design入門

最近Atomic Designという考え方を学んだので、コンポーネント指向と絡めつつ僕なりの理解をアウトプットしてみます。
Atomic Designもコンポーネント指向も初心者なので、誤りがあれば指摘してもらえると助かります。

Atomic Designとは

Atomic DesignとはUI設計の考え方で、「小さなUIコンポーネントを組み合わせて1つのページを作っていこう」というもの。
僕のざっくりとした理解では、「小さなパーツを組み合わせ、徐々に意味のある大きな要素を作っていき、最終的に1つのページになる」みたいな感じです。

5段階で捉えるUI構造

Atomic Designで有名なのは、UIの構造を次の5段階に落とし込むという考え方。
atomic_design.png

引用元:Atomic Design Methodology

atoms

原子。それ以上分解できない最小単位。
ラベル、ボタン、アイコンなど。
ヘッダーを例にとるなら、ここがatoms。
スクリーンショット 2020-12-14 17.33.44.png

molecules

分子。atomsを組み合わせてできたもの。
ヘッダーナビゲーションや検索フォームなど。
ヘッダーを例にとるなら、ここがmolecules。
スクリーンショット 2020-12-14 17.38.10.png

organisms

生物。atomsやmoleculesを組み合わせてできたもの。意味を持っている単位。
ヘッダーそのものがorganisms。
atomsとmoleculesが組み合わさってヘッダーになる。
image.png

templates

骨組み。例えば、ヘッダーとサイドナビゲーションは全てのページで固定化し、メインコンテンツだけが異なるページを作っていく場合などに便利。
ここに関しては後半でもう一度紹介します。
スクリーンショット 2020-12-14 17.39.51.png

pages

ページ。atoms, molecules, organisms, templatesが組み合わさって1つのページとなる。
image.png

Atomic Designを採用すると何が嬉しいか?

Atomic Design/コンポーネント指向を採用するメリットはこんな感じかなと思います。
1. 再利用性が高まる
2. 小さな単位で問題を捉えられる
3. 全体のデザインに一貫性が出る

1. 再利用性が高まる

ページ内で何度も出てくるようなUIパーツは、コンポーネントとしてまとめておけば使い回すことができます。

2. 小さな単位で問題を捉えられる

コンポーネント指向の強みは疎結合な状態を作り出せることです。(コンポーネント同士の結びつきが弱く独立性が高い状態)。そのため、問題が発生したときに(変更したい/取り除きたいなど)、単位を小さく捉えることができ解決しやすくなります。

3. 全体のデザインに一貫性が出る

コンポーネントを使い回すことで、「このボタンだけデザインがおかしい」みたいなことが防げるので、全体に一貫性が出ます。

コンポーネント指向でファイルを分割すると何が嬉しいか?

Atomic Designのメリットがわかってきたところで、具体例を交えながらコンポーネント指向のメリットも見ていきます。

例1. ここのボタンの色変えたい...

スクリーンショット 2020-12-14 17.48.55.png

コンポーネントとしてこのボタンを作っておけば、ボタンコンポーネントの色を変えるだけで全てのボタンにその変更が反映されます。もしコンポーネントに分けていなかったら、1つずつボタンを定義しているコードを探し出して、それを1つずつ直していかないといけません。このページのボタンだけならまだ楽ですが、ページの数・ファイルの数が増えてくるといちいち探して直していくのはかなり面倒になってきます。

例2. この部分差し替えたい...

スクリーンショット 2020-12-14 18.06.13.png

ページのこの部分を丸ごと別の要素に差し替えたいみたいな場合。コンポーネントとして独立させておけば、それを入れ替えるだけで目的は達成されます。しかし、コンポーネントにしていなければ、ファイルの中からこの部分を探し出さなければいけません。面倒ですね。

例3. ページに応じてコンテンツだけを変更したい...

スクリーンショット 2020-12-14 17.39.51.png

ヘッダーとサイドナビゲーションは他のページでも同様のレイアウトを保ちたいという場合。ページごとで毎回それを書いていたら無駄が多くなってしまいます。そんな時はテンプレートを作ってコンテンツだけ差し替えられる形にするのが良さそうです。Vue.jsのvue-routerなどはこの考えに基づいて実装していくイメージです。

Atomic Design/コンポーネント指向のデメリット

ここまで良いところばかりを紹介してきましたが、デメリットもあります。
1. ファイル数が激増する
2. 開発メンバー間で理解の齟齬があると辛い
3. エンジニア・デザイナー間のコミュケーションの難しさ

1. ファイル数が激増する

小さなコンポーネントもどんどん切り分けていくとファイル数が激増します。1フォルダで管理しきれないほどのファイル数になる場合は、フォルダを切り分けていくのも1つの解決策かなと思います。

2. 開発メンバー間で理解の齟齬があると辛い

チーム開発する際に、コンポーネントの単位をチームメンバーとすりあわせておかないと、逆にごちゃごちゃしてしまう可能性があります。開発を始める前、コンポーネントの単位に悩んだ時は、チームで意見のすり合わせをした方がいいかなと思います。

3. エンジニア・デザイナー間のコミュニケーションの難しさ

Atomic Designはどちらかというと開発者寄りの考え方かなと思います。Atomic Designの考え方を汲んでいるデザインはやはり開発しやすいです。Atomic Designを理解していないデザイナーさんと一緒に仕事する場合は、UI設計について少し議論する必要もあるのかなと思います。

まとめ

Atomic Designとコンポーネント指向について見てきました。
Vue.js, React, Angularなど、フロントエンドの技術はコンポーネント指向に基づく実装がデファクトスタンダードになりつつあります。なので、このようなUI設計の考え方も重要性が増してきている気がします。

UI設計の考え方は他にも色々あるので、すべての場合にAtomic Designが有効かというと、そうではないと思います。さらにAtomic Designの中でも、「どの要素をmoleculesやorganismsの範疇に含めるのか」、「ディレクトリ構成はどうしていくのか」など常につきまとう問題がいくつかあります。最後まで気持ちよく開発をしていくためにも、チームメンバーと理解をすりあわせ、適切な判断をしていきたいですね。

参考

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

TypeScript+React+Cloud Firestore+Stateモナドで、ブラウザで動くボードゲームの対戦ツールを作った (1/3 概要編)

概要

image.png

  • 好きなボードゲーム(ナショナルエコノミー)のオンライン対戦環境が欲しかったので作った。
  • 遊びたいときに手軽に共有したかったので、ブラウザで動くものとした。
  • バックエンドを用意したり書いたりするのが面倒であり、また公開するつもりもなかったので、Cloud Firestoreでデータベースを用意して、ロジックはすべてフロントに書くサーバレス構造にした。
  • ゲーム中の状態を丸ごとDBに突っ込んで全プレイヤー(および観戦者)に共有し、行動によるゲーム状態変化をすべてStateモナドによって記述した。これによって、様々な種類が存在するゲーム内効果を平易に、かつ再利用性とテスタビリティが高い形で記述できるようにした。
  • フロントエンドはReactで記述した。
  • Webフロントエンド業務においてTypeScriptを利用しているので、その流れで全体をTypeScriptで記述した。
  • 本記事は概要編として全体の構造と、ゲーム状態エンティティについての説明を行う。
  • 全記事を読むことで、同様の構造を用いて様々なゲームのオンライン対戦環境を作成できるようになる。(たぶん)

注意

本記事で作成したオンライン対戦環境は、特に原作者に許諾を取って作成したものではなく、一般に公開もしていません。
抽象的な「ゲームのルール」は表現物ではなく、著作権による保護対象とはされていません。したがってゲームのアートや文章を流用しなければ、たとえ原作者に許諾を得ずにオンライン対戦環境を公開しても、知的財産権を侵害することはないと考えられます。

とはいえ、この対戦環境を作成したきっかけが、そもそも身内でのみ楽しみたいというものであったこと、また単純に気が引けたため、特定少数にのみ共有して遊んでいました(現在は動かしていません)。
本記事は技術的な側面についてのみ記述しています。

はじめに

環境

一般公開の予定がなく、ゲーム好きの友人とのみ遊べればそれでよかったので、Windowsにおいて新しめのバージョンのGoogle Chromeで動けばとりあえず問題ないだろう、という雑な想定で、各種ライブラリを適当に引っ張ってきました。そのため、本項目についてはあまり厳密に見ないことを推奨します。

  • TypeScript 3.8.3
  • Firebase JavaScript SDK 7.14.0
  • React 16.13.1
  • Mocha 7.1.1
  • power-assert 1.6.1
  • その他開発環境のためのwebpackとかいろいろ

ゲームについて

本記事で対象とする非電源ゲームナショナルエコノミーは、スパ帝国が作成し販売するゲームです。
プレイヤーは事業家となり、雇用した従業員を職場へ派遣したり、あるいは職場を作成して、彼らに給料を支払いつつ事業を拡大します。これによって最も多くの資産を形成した者が勝者となります。
このゲームをオンライン対戦環境作成の対象とした理由は、単に気に入ったので遊びたかったというごくシンプルなものですが、それ以上にこのゲームはオンライン対戦環境を作成する上で、以下の有利な特徴を持っています。

  • いわゆるワーカープレイスメントであり、各プレイヤーの手番が独立していて他プレイヤーの行動に対する割り込みが存在しない。そのため、リアルタイム性が要求されず、メカニクスの実装が容易である。
  • プレイヤー間の貿易といった、テキスト/ボイスチャットだけで完結しない交渉要素が存在しない。そのため、純粋にゲームメカニクス部分だけを実装すればシステムが完結する。
  • すべてのコンポーネントがカードで構成されており、いわゆるゲームボードやトークン類がない。そのため、UIが作成しやすい。

非電源ゲームの中には、人狼系ゲームのように数値的・記述的なメカニクスでなくコミュニケーションを主とするものや、他プレイヤーの行動に割り込むような動作が許可されるものも数多く存在します。このようなゲームのオンライン対戦環境作成においては、単にメカニクスを実装するのみならず、フレキシブルな交渉システムやラグの考慮など、実装難易度を引き上げる要素が存在するでしょう。

記事中では、特に説明なくこのゲーム内の概念・呼称を用いることがあります。厳密なルールはルール説明や各種レビュー、プレイ動画などを参照してください。厳密なルールを把握しなくても、技術的な要素については理解できるような書き方になるよう努めています。

全体構造

本環境においては、次のような関数型言語ガチ勢にツッコまれそうな構造でオンライン対戦機能が実現されます。

image.png

Cloud FireStoreにはゲームの全状態$s\in S$が置かれています。この状態$s$は、プレイヤーがhtmlを開いたとき、(1)Firebase SDKを通じて取得され、以降リアルタイムで自動的に同期されます。
取得した状態$s$に応じて、(2)Reactのコンポーネントがゲーム状態をDOMとして出力し、プレイヤーはそれを画面で見ることになります。
状態$s$がそのプレイヤーから見て「手出しできない」、つまり他プレイヤーの手番であったり、何かの処理待ちであったりする場合にはこれで終わりです。

image.png

しかし、何らかのゲーム的に合法な操作が可能である場合、Reactコンポーネントはその部分についてクリックなどの操作が可能であるようDOMを出力しています。これに(3)プレイヤーが操作を行うと、(4)Reactコンポーネントは操作に対応するStateモナド$M_{Log, S}: S \rightarrow (Log, S)$をロジックに問い合わせます。

image.png

(5)得られたStateモナドを現在のゲーム状態$s$に適用(run)すると、新状態$s'$(と操作ログ)が得られます。これをふたたび(6)Firebase SDKを通じてCloud FireStoreに戻せば、プレイヤー操作による状態更新が完了し、他プレイヤーにも自動的に新たな状態が同期されます。

なぜこのような構造にしたか?

ボードゲームにおける様々な操作は、共通点の多い、より小さな要素の結合とみなせることがしばしばあります。特に、カードベースのワーカプレイスメントであるナショナルエコノミーは、そのような小要素を非常に多く見出せます。

例えば、「山札からカードを1枚引く」という操作は、

  1. 山札の上から1枚のカードを取り除く。取り除かれたカードとともにその後の状態を返す
  2. カードを受け取って、手札にそれらのカードを加えた状態を返す

という小要素の結合であるとみなすことができます。これらはStateモナドか、または「ある引数を受け取ってStateモナドを返す関数」であるため、結合は非常に容易であり、単にbindするだけです。

また、これらの小要素は、それぞれを単独で生成し、適当な$s_{\mathrm{test}}\in S$にrunして、得られた$s_{\mathrm{test}}'$を理想の結果$s_{\mathrm{expected}}$と比較することで、簡単に単体テストを書くことができます。
もちろん、その結合である「手札からカードを2枚捨て、山札からカードを4枚引く」操作のStateモナドに対しても同様に単体テストを行えます。

そして、これらの小要素の一部は、さらに別の操作「手札からカードを2枚捨て、山札からカードを4枚引く」に再利用することができます。
単体テストされた部分操作Stateモナドの再利用を繰り返して、最終的にはゲーム内のすべての操作を記述できるため、ゲームメカニクスの実装が非常に堅牢かつ高速になります。
ゲームメカニクスのバグは発見も修正も非常にコストが大きいため、この再利用性とテスタビリティは見逃せないでしょう。

さらに、ゲームメカニクスの実装と完全に分離した形で、UIや通信部分を実装・テストすることができます。メカニクス部のテストと同様に、適当な状態$s\in S$を表示したり、通信したりすればよいだけだからです。

エンティティ構造

ゲームの各種状態、すなわちプレイヤー手札だの場札だの所持金だのを格納するためのエンティティはtypeで定義します。
これらのオブジェクトがCloud Firestoreからリアルタイム共有されることでゲームの全状態が判明し、必要なものをUIで表示します。
操作によって新たな状態へ遷移する際には、変数の書き換えではなく単に新状態オブジェクトを作成することで行い、実質的な「書き換え」はCloud Firestoreへの送信によってのみ行います。
そのため、メカニクス部にはvarletも一切使われません。

メンバの型

エンティティの保持をCloud Firestoreのみが行う以上、エンティティのメンバはすべて保持可能なものである必要があります。すなわち、数値か文字列、あるいはその配列やオブジェクトのみが許されるという意味です。
とはいえ、ボードゲームの状態を記述するエンティティに関数が必要になることはないはずです。たとえば、プレイヤーの状態は次のように記述されます。

player.ts
import { CardName } from "./card";
import { Building } from "./building";

export type PlayerIdentifier = "red" | "purple" | "orange" | "blue";
export type Player = {
    id: PlayerIdentifier;
    name?: string;
    hand: CardName[];
    workers: {available: number, training: number, employed: number};
    buildings: Building[];
    cash: number;
    victoryToken: number;
    reservedCards: CardName[];
    penalty: number;
};

CardName型は単にカード名文字列を並べた共用体型であり、DB上では文字列で扱われます。Building型はプレイヤーが建てたり、公共の建物として用意される「職場」であり、説明は省きますがやはり文字列だけで表現できるように定義されています。

また、これらを格納するための「盤上の全状態」が次のように記述されます。

board.ts
import { Player, PlayerIdentifier } from "./player";
import { Building } from "./building";
import { CardName } from "./card";

export type Board = {
    currentRound: number;
    deck: CardName[];
    trash: CardName[];
    houseHold: number;
    players: {[index: number]: Player};
    startPlayer: PlayerIdentifier;
    publicBuildings: Building[];
    soldBuildings: Building[];
};

このBoardはさらに、ゲームの全状態を格納するGame型のメンバとなり、それがまとめてCloud Firestoreを通じて全ユーザーに共有されます。

「操作待ち状態」の表現

ボードゲームにおいて物理的に表現される状態は、ルールどおりに数値や文字列を定義することで実装できます。一方で、「プレイヤーが効果の選択肢が複数あるような操作を行った。現在、当該プレイヤーはどの選択肢を選ぶかの待ち状態である」という表現も状態変数の中に盛り込みたいところです。それには次のような理由が挙げられます。

  • 「山札の上から複数枚見て、そのうち1枚を手札に加える」という操作があったとき、その途中状態に遷移したことをDBに知らせない、すなわち手番プレイヤーのローカルでのみ状態を保持している場合、手番プレイヤーは次の選択操作を行わずにページを一度閉じることで、実質的に山札の上を見たうえでキャンセルができてしまう。
  • 操作待ち状態の進捗も他プレイヤーに共有することで、待ち時間のストレス軽減につながる。
  • 複数プレイヤーが同時に進行できる操作があるとき、各プレイヤーの状態を確認しつつゲームプレイが高速化できる。

このツールでは、次のように「操作待ち状態」を定義しています。

game.ts
import { Board } from "./board";
import { InRoundState, ExRoundState, ResultState } from "./state";

export type Game = {
    board: Board;
    state: InRoundState | ExRoundState | ResultState;
};
state.ts
import { PlayerIdentifier } from "./player";
import { CardName } from "./card";

export type InRoundState = {
    currentPlayer: PlayerIdentifier;
    phase: "dispatching" | "oncardeffect";
    effecting?: CardName;
    revealing?: CardName[]
};

export type ExRoundPlayerStatus = "selling" | "discarding" | "confirm" | "finish";
export type ExRoundState = {[key: string]: ExRoundPlayerStatus};

export type PlayerResult = {
    cash: number;
    buildings: number;
    victoryToken: number;
    penalty: number;
    bonus: number; 
    total: number;
};

export type ResultState = {
    winner: string;
};

Game型のstateInRoundState(プレイヤー手番の行動選択または効果選択待ち)、ExRoundState(ラウンド終了時の清算処理における各プレイヤーの操作待ち)、ResultState(結果表示)のいずれかを取ることにより、今誰が何の操作を行えるのかが判断できるようになっています。

まとめ

本記事では、ボードゲームのオンライン対戦環境作成において、そのさわりとなる概要とエンティティの説明を行いました。
次回はコアロジックであるゲームメカニクスを、Stateモナドを用いて実装する手法について解説します。

参考文献

Haskell 状態系モナド 超入門
Cloud Firestore

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

React+Firestoreでブックマーク(お気に入り)の保存機能を作成する

現在ReactとFirestoreで作成しているブログのようなwebサービスの各投稿をブックマークして、別ページで閲覧できるようにするような機能を追加したので、その解説をします。

前提

解説をする上で、前提があります。

  • ReactのFunction ComponentやHooks、contextをなんとなくでも使える段階にある
  • Firestoreのcollectionやdocument、subcollectionについてそれらがどういうものかぼんやりとでも知っている
  • Firestoreのデータを読み込んだり書き込んだりできる

上記の二つが前提となる知識です。

さらにこの記事では大まかな実装の流れや考え方を解説するのでコードを省略している部分もあります
この記事だけで実装はできませんが、ブックマーク機能やお気に入り機能を実装する上でのヒントになると思っています。(人それぞれコードや実装の方法が違いますからね?)

機能について

今回はブックマーク機能を実装すると言いましたが、どのようなものなのかは以下の画像をご覧ください。

スクリーンショット 2020-11-25 16.06.58.png

各投稿に対し、右下にブックマークのiconを表示します。
このiconをクリックし、ブックマーク登録がされると色がつき、もう一度クリックすると枠だけのiconに戻ります。

このブックマークのiconにはmaterial uiのiconを使います。

Material UIの導入についてはこちらの記事が参考になると思います。

また、保存した記事は画像の上部のheaderにある紙のようなiconをクリックすることで保存した記事を一覧表示するページに飛べます。

実装

さて、早速実装していきましょう。

db

まず、Firestoreのdb設計についてですが、usersコレクションの中にある各ユーザーに対して、サブコレクションとしてbookmarksという名前のものを追加し、そこにブックマークした記事を保存していきます。

usersコレクション→それぞれのuserのドキュメント→bookmarksというサブコレクション→bookmarksのドキュメント

users - userID - bookmarks - document - id
                                      - post_id
                                      - authoName
                                      - createdAt
                                      - title     

投稿に対して、ブックマークを押したら現在ログインしているユーザーのデータに、ブックマークした記事のデータが保存される感じです。

一覧画面でのブックマーク

今回は、各投稿に対してブックマークボタンを配置しているので、投稿のcardを生成しているコンポーネントにブックマークのiconを追加する必要があります。

僕の場合はPostコンポーネントをforEachで回して記事を表示しているので、Postコンポーネントにiconを追加します。

Post.jsx
//importやstyle含め、いろいろ省略しています。

const Post = ({ authorName, content, createdAt, title, id, uid}) => {
  const { currentUser, savePostToBookmark, removePostFromBookmark } = useAuth();
  const [saved, setSaved] = useState(false)

  //ここからが該当の場所です!!
  const savePost = () => {
    setSaved(true)
    const savedPosts = ({ authorName, content, createdAt, title, id});
    return savePostToBookmark(savedPosts)
  }

  const removeBookmark = async (id) => {
    setSaved(false)
    removePostFromBookmark(id)
  };

  //useEffect内では、ログアウトして再度ログインした際や、画面を再読み込みした際などにも
  //ブックマークした投稿が消えないようにするために、ログインユーザーのサブコレクションbookmarksを参照し、
  //もしpostのidと保存された投稿のpostのidが等しければsaved=trueとすることでブックマークされた状態を表示するようにしています。
  useEffect(() => {
    const uid = currentUser.uid
    db.collection('users').doc(uid).get()
    .then(doc => {
        if (doc.exists) {
          db.collection('users').doc(uid).collection('bookmarks').get()
          .then(snapshots => {
            snapshots.docs.forEach(doc => {
              const data = doc.data();

              const post_id = data.id
              const saveId = data.saveId
              if (saveId === id) {
                 setSaved(true)
                 setSavedId(saveId)
              }
            })
          })
        }
      })

  }, [saved])
  //ここまで

    return (
        <>
        <Card className={classes.root} variant="outlined">
            <CardContent style={{ paddingBottom: "0" }}>           
            {/*ここからが該当の場所です!!*/}
            {saved === true ? 
            <IconButton className={classes.likeBtn} onClick={removeBookmark}>
              <BookmarkIcon />
            </IconButton>
            :
            <IconButton className={classes.likeBtn} onClick={savePost}>
               <BookmarkBorderIcon />
            </IconButton>
            }
            {/*ここまで*/}
        </Card>
        </>
    )
}

11/26 追記

Post.jsxのuseEffectの第二引数(deps)にsavedを追加しました。depsは空配列の場合、マウント時、アンマウント時にuseEffectの第一引数を実行しますが、この場合、ブックマークをクリックした時に、そのブックマークのidを保存するサブコレクションのidが取得できておらず、何かPostコンポーネントが再びマウントされるような操作を行わない限り、idが取得できていない状態なので、firebaseの参照エラーが起きます。なので、今回はブックマークされるタイミングで、savedがfalseからtrueになるのをdepsで感知して、useEffectを実行するようにしました。追記以上です。

ブックマークされている状態のときは、<BookmarkIcon />
ブックマークされていnaい状態のときは、< BookmarkBorderIcon />
を表示します。

では、どのようにしてブックマークされているのかどうかを判断しているのかというと、savedというstateのtrueかfalseによって判断しています。const [saved, setSaved] = useState(false)の部分で定義しています。初期値、つまり何もしていない状態ではもちろんブックマークは登録されていないので、savedの初期値はfalseを入れています。ブックマークが登録されたタイミングで、savedがtrueとなり、iconに色がつきます(正確には塗り潰されたiconに入れ替わる)。

ブックマーク登録をし、dbにデータを追加する

ブックマークが登録されていない状態で、iconをクリックすると、savePostという関数が走ります。
この関数ではブックマークを押した投稿のデータをdbに保存します。もう一度コードを記載します。

Post.jsx
 //クリックされたらこの関数が走る
  const savePost = () => {
    //まずはsavedをtrueに
    setSaved(true)
    //savedPostsはクリックされたiconの投稿の情報
    const savedPosts = ({ authorName, content, createdAt, title, id});
    //savedPostsをsavePostToBookmarkに渡して実行
    return savePostToBookmark(savedPosts)
  }

ここでのsavePostToBookmarkは、contextを用いて別のファイルで書いた関数です。内容は以下の通り。

context.jsx
  const savePostToBookmark = async(savedPosts) => {
    const uid = currentUser.uid
    //saveRefではサブコレクションbookmarksを参照し、
    const saveRef = db.collection('users').doc(uid).collection('bookmarks').doc();
    //以下のコードで、savedPostsに自身のid(bookmarksのdocument id)を追加します。
    savedPosts['saveId'] = saveRef.id;
    //Post.jsxから渡ってきた情報(savedPosts)を保存します。
    await saveRef.set(savedPosts);
  }

これにてブックマークの登録は完了です。

ブックマーク登録を解除し、dbからデータを削除する

ここでは削除機能を実装します。

Post.jsx
  const removeBookmark = async (id) => {
    //まずはsavedをtrueに
    setSaved(false)
    //contextの関数にデータを渡す
    removePostFromBookmark(id)
  };

ここでも、removePostFromBookmarkという関数をcontextから呼び出して使っています。

context.jsx
  const removePostFromBookmark = id => {
    const uid = currentUser.uid
    //Post.jsxから渡ってきたデータ(今回はid)を元にdbから該当のデータを探し出し、delete()でデータを削除
    db.collection('users').doc(uid)
        .collection('bookmarks').doc(id)
        .delete();
  };

ブックマーク済みのiconをクリックするとremoveBookmarkが実行され、ブックマークのデータがdbから削除されます。この時にsetSaved(false)でsavedをfalseとして表示されるbookmarkのiconを切り替えます。

以上がブックマークの登録と削除でした。

ブックマークした投稿を別ページで表示しよう

ここでは、ブックマークした投稿を別のページで一覧表示する機能と、その画面でブックマークを解除した時に一覧から投稿が消えるような機能を実装します。

スクリーンショット 2020-11-25 17.46.21.png

ブックマークされた投稿を取得し表示する

ブックマークされた投稿を表示します。各投稿は子のコンポーネントとして別でコンポーネントを作成し、mapで回して表示します。(ブックマークした投稿が一つだけでも配列として保存しているのでmapかforEachする必要があります)

BookmarkList.jsx
export default function BookmarkList() {
    const { currentUser } = useAuth()
    //dbから取り出した各bookamarkのデータを保持するためのstateを用意する
    const [bookmarks, setBookmarks] = useState([])

    useEffect(() => {
        const uid = currentUser.uid;
        let posts = [];
        //サブコレクションbookmarksを取得し
        db.collection('users').doc(uid).collection('bookmarks').get()
            .then(snapshots => {
                //forEachで中身を取り出す。
                snapshots.docs.forEach(doc => {
                    const data = doc.data()
                    //取り出したデータをpostsという配列にpush
                    posts.push({
                        authorName: data.authorName,
                        content: data.content,
                        createdAt: data.createdAt,
                        title: data.title,
                        saveId: doc.id,
                        uid: data.uid,
                        post_id: data.id
                    })

                })
                //用意したbookmarksにstateでpostsのデータを保持する
                setBookmarks(posts)
            })
    }, [])

    return (
        <>
            <div style={{ marginTop: "100px" }}>
            <h3>保存した投稿</h3>
            {/*stateのbookmarksはforEachで回した分データを配列として持っているので、mapで取り出す*/}
            {bookmarks.map(bookmark => 
               {/*BookmarkListItemにデータを渡す*/}
                <BookmarkListItem 
                    key={bookmark.saveId}
                    authorName={bookmark.authorName}
                    content={bookmark.content}
                    createdAt={bookmark.createdAt}
                    title={bookmark.title}
                    // bookmarkのid
                    id={bookmark.saveId}
                    post_id={bookmark.post_id}
                />
            )}
            </div>
        </>
    )
}

続いて、BookmarkListItemです。
既にブックマークをされている投稿を表示しているので、ブックマークに追加するためのiconや関数は用意していません。

BookmarkListItem.jsx
export default function BookmarkListItem({ authorName, content, createdAt, title, post_id, id}) {
    const { removePostFromBookmark } = useAuth()
    const [saved, setSaved] = useState(true)

    //ここではcontextから読み込んだブックマークを解除する関数を実行するためのコードを書きます。
    const removeBookmark = () => {
        //まずはsavedをfalseに
        setSaved(false)
        //contextから読み込んだremovePostFromBookmarkにidを渡し、ブックマークのデータをdbから削除
        removePostFromBookmark(id)
    };

    return (
        <>
        {/*このコンポーネントはブックマーク登録されていないものは表示する必要がないの*/}
        {/*そこで全体を条件式で囲ってsaved=trueの場合(つまり、ブックマークされている場合)のみ表示するようにしている*/}
        {saved === true && 
        <Card className={classes.root} variant="outlined">
            <CardContent style={{ paddingBottom: "0" }}>
                <Typography variant="h5" component="h3">
                  {title}
                </Typography>
                <Typography className={classes.pos} color="textSecondary">
                  {authorName+""+createdAt}
                </Typography>
                <Typography className={classes.contentText} variant="body2" component="p">
                  {content}
                </Typography>
            </CardContent>
            <CardActions className={classes.detailBtnWrap}>
              <Link to={'/detail/' + post_id} className={classes.detailLink}>
                <Button variant="contained" className={classes.detailButton} size="small">詳細を表示</Button>
              </Link>
            </CardActions>
            <IconButton className={classes.likeBtn} onClick={() => removeBookmark(id)}>
               <BookmarkIcon />
            </IconButton>
        </Card>
        }
        </>
    )
}

こんな感じで実装は完了です。

最後に

いろいろ省略して解説してきましたが、ブックマークやお気に入りしてそれらを一覧表示したいという方の役に立てたら幸いです。

また、「ここはこういう書き方の方がいい」みたいなコードの提案や、間違ってる部分の指摘など、コメントでお待ちしております。

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

[ReactNative] ScreenView+Webview refreshControlについての備忘録(Androidのバグ)

最近小さめのReactNativeのプロジェクトにアサインされたワタクシでございます。
全くまだ分かってないんですけど、UI部分の改修案件が来たので対応しています。
その際に直面したバグ対応の備忘録を残します。

どのようなバグか

iosでは起きず、Androidのみのバグとなります。

render() {
  <ScrollView
     refreshControl={
       <RefreshControl
         refreshing={ this.state.refreshing }
         style={{ backgroundColor: "#FFFFFF" }}
         onRefresh={() => { this.onRefresh() }}
       />
     }
     contentContainerStyle={{flex: 1}}
     style={{ backgroundColor: "#FFFFFF" }}
   >
            <WebView
              style={{ flex: 1 }}
              onMessage={this.onMessage}
              onNavigationStateChange={this.onNavigationStateChange}
              onLoadStart={this.onLoadStart}
              onLoadEnd={this.onLoadEnd}
              ref={webView => {this.webView = webView}}
              source={{ uri: this.state.url.toString() }}
            />
    </ScrollView>
}

このようなソースがあります。
webviewを表示しており、このwebviewはスクロールが可能になっています。
スクロールをする際にiosでは起きませんが、Androidではスクロールで下までいってしまったあとに上に戻ろうとすると必ずrefreshControlが走り、上にスクロールが出来なくなるバグが起きてました。
アプリの上部に来た時だけ更新してくれればいいのに、常に上部をひっぱると更新されてしまいます。

原因

webview側は問題なくスクロールします。スクロールされているのはScrollViewではなくて、webviewであることがわかり、(ScrollViewにonScrollつけてもイベントが走らなかったため)ScrollViewのscrollYが更新されず0のままでした。
そうすると恐らく、scrollYが0であるとしてAndroidでは常にrefreshControlが走ってしまうのではないか、というところまでたどり着きました。

開発環境

  • react-native: 0.61.5
  • react: 16.9.5
  • react-native-webview: 8.0.3

解決方法

const INJECTED_JS = `
  window.onscroll = function() {
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        scrollTop: document.documentElement.scrollTop || document.body.scrollTop
      }),
    )
`

export default class WebViewPage extends React.Component {
  state = {
    scrollViewHeight: 0,
    isPullToRefreshEnabled: false,
  }

  onMessage = () => {
    const { data } = e.nativeEvent

    try {
      const { scrollTop } = JSON.parse(data)
      this.setState({ isPullToRefreshEnabled: scrollTop === 0 })
    } catch (err) {
      // ...
    }
  }

  render () {
    const { scrollViewHeight, isPullToRefreshEnabled } = this.state

    return (
      <View style={{flex:1}}>
        <ScrollView
          refreshControl={
            <RefreshControl
              refreshing={ this.state.refreshing }
              enabled={ isPullToRefreshEnabled }
              style={{ backgroundColor: "#FFFFFF" }}
              onRefresh={() => { this.onRefresh() }}
            />
          }
          onLayout={e => this.setState({ scrollViewHeight: e.nativeEvent.layout.height })}
          contentContainerStyle={{flex: 1}}
          style={{ backgroundColor: "#FFFFFF" }}
        >
          <WebView
            style={{ flex: 1 }}
            onMessage={this.onMessage}
            onNavigationStateChange={this.onNavigationStateChange}
            onLoadStart={this.onLoadStart}
            onLoadEnd={this.onLoadEnd}
            ref={webView => {this.webView = webView}}
            source={{ uri: this.state.url.toString() }}
            injectedJavaScript={INJECTED_JS}
          />
        </ScrollView>
      </View>
    )
  }
}

解説

  1. ScrollViewでスクロールイベント取れないならwebviewで取るしかないよなということで injectedJavaScript を導入。injectedJavaScriptはwebview側にjavascriptのコードを渡す属性で、ここでwebviewのスクロール位置を取得して、window.ReactNativeWebView.postMessage でReactNative側にその情報を渡してやります。この時、webview側にもonMessageメソッドを定義して、webview側のスクロール位置情報を受け取ります
  2. enabledでいつrefreshControlが走るか定義しておく(onMessage)
  3. ScrollViewコンポーネントのonLayoutを使って、viewが開いたときの高さを取得して保持しておきます。(初回だけ取得用)
  4. WebviewコンポーネントにinjectedJavaScriptを突っ込みます。

以上です。これで画面の一番上にスクロールした時にだけ上部を引っ張ればrefreshControlが走る設計になったかと思います。

参考文献

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

【React】ユーザー認証をCognito + Amplifyで構築してみた ~ 構築準備編 ~

はじめに

Reactで作成したWebアプリケーションのユーザー認証部分をCognito + Amplifyフレームワークで構築してみました。
本記事では、それぞれのサービスについて、Amplifyの設定方法についてまとめています。

完成画面

今回は、ユーザー名、パスワードとワンタイムパスワードの2段階認証を行います。そして、サインインをしたユーザーを識別し、トップページには「{ユーザー名}さんこんにちは!」という文字が出るようにします。

ezgif.com-gif-maker.gif

方法検討

要件

認証方法を考えるにあたり、条件は以下の通りです。

  • 静的コンテンツをS3に置いている
  • アプリケーション部分はLambdaで実装している
  • アプリケーション内でユーザーを管理しており、ユーザーに登録されている人だけ使えるようにしたい
  • 今後ユーザーのマイページも作りたいので、認証と合わせてユーザーを識別したい
  • パスワードはシステム管理者が管理しないようにするのがベター
  • ソーシャルアカウントは使わない
  • MFA(多要素認証)を使いたい

結論

この条件に沿って認証部分を考えた結果、Cognitoを利用することにしました。また、Cognitoと既存のWebアプリケーションを簡単に連携できるAWSのAmplifyフレームワークというものがあったので、こちらも使ってみることにしました。

使用するAWSサービスについて

Cognitoってなに?

公式サイトによると、次のように説明されています。

Amazon Cognito を使用すれば、ウェブアプリケーションおよびモバイルアプリに素早く簡単にユーザーのサインアップ/サインインおよびアクセスコントロールの機能を追加できます。Amazon Cognito は、数百万人のユーザーにスケールし、Facebook、Google、Amazon などのソーシャル ID プロバイダー、および SAML 2.0 によるエンタープライズ ID プロバイダーを使用したサインインをサポートします。

Cognitoでできること

今回のCognitoでやることは下記の通りです。

  • Webアプリケーションのユーザーの管理(パスワードなど含む)
  • ユーザーがサインインするときの認証

料金

料金は使った分だけです。最低料金もかかりません。
今回のように、ユーザープールの認証情報を使ってCognitoユーザープールに直接サインインする場合は50,000MAUの無料枠があります。50,000MAUを超えるアクティブユーザーがいる場合は、0.0055USD/MAUかかります。(人数幅によって多少減額あり。詳細はこちらをご覧ください。)
※1MAU=サインアップ、サインイン、トークンの更新またはパスワードの変更など、当該ユーザーに関わる ID 操作があったユーザー(アクティブユーザー)

参考:https://aws.amazon.com/jp/cognito/pricing/

Amplifyってなに?

公式サイトによると、次のように説明されています。

AWS Amplify は、モバイルとウェブのフロントエンドデベロッパーが、安全でスケーラブルなフルスタックアプリケーションを構築しデプロイできるようにする、AWS によるエンドツーエンドのソリューションです。Amplify を使用すれば、アプリケーションのバックエンドを数分で設定し、わずか数行のコードでそれをアプリケーションに接続できます。そして、3 ステップで静的なウェブアプリケーションをデプロイできます。

その中で、分析や認証などの機能を持つAWSリソースを構成するのに役立つ対話型のCLIを提供していたり、認証処理などの実装や画面の作成に使うライブラリ、UIコンポーネントを提供しているAmplifyフレームワークというものを使います。

Amplifyフレームワークでできること

今回Amplifyフレームワークでやることは下記の通りです。

  • Amplify CLIを使ってのCognitoユーザープールの構築
  • 既存のソースコードとCognitoの連携
  • サインイン画面作成
  • ユーザー情報の取得、サインアウト機能の実装

今回はフロントエンドフレームワークとしてReactを使いましたが、他にもReact Native、Angular、Vue、IonicやiOS/Android IDEでも使うことができます。

料金

Amplifyフレームワーク自体には料金はかかりません。

Amplify フレームワーク (ライブラリ、CLI、UIコンポーネント) を使用する場合は、基盤として使用する AWS のサービスに対してのみお支払いいただきます。Amplify フレームワークの使用には、追加料金は発生しません。

参考:https://aws.amazon.com/jp/amplify/pricing/?nc=sn&loc=3

手順

下記の流れで進めていきます。

本記事ではこちらの手順についてまとめています。
1. Reactの開発環境の構築
2. Amplifyの設定
3. Amplify用のIAMユーザーの作成
4. Amplifyの初期設定

下記の5番、6番については別記事 構築完成編 へ。
5. Cognitoユーザープールの作成
6. コード実装

やってみる

1. Reactの開発環境の構築

React環境を簡単に構築する環境構築ツールはcreate-react-app、パッケージマネージャーはyarnを使いました。

参考:新しい React アプリを作る

バージョン

  • node 14.15.0
  • yarn 1.22.10
  • create-react-app 4.0.0

2. Amplifyの設定

AmplifyをCLIで操作するために、Amplify CLIをインストールします。

$ yarn global add @aws-amplify/cli

そして、プロジェクトには、Amplifyのパッケージを追加します。

$ yarn add aws-amplify
$ yarn add aws-amplify-react
$ yarn add @aws-amplify/ui-react

3. Amplify用のIAMユーザーの作成

AWSでAmplifyが使うIAMユーザーの作成し、名前付きプロファイルにIAMユーザーを設定します。

$ amplify configure

上記のコマンドを実行すると、AWSのコンソールが自動で開くのでコンソールでIAMユーザーを作成します。その後、コマンドラインで対話形式で作成したIAMユーザーの認証情報をAWS CLIのcredentialsファイルに保存します。今回、プロファイル名はamplify-cognitoで保存しました。

詳しくは、こちらの記事のAWSアカウントの紐付けを参考にしました。
AWS Amplify CLIの使い方〜インストールから初期セットアップまで〜

ただ、Amplify用のIAMユーザーにAdministratorAccessのポリシーをアタッチするのは嫌だったので、こちらを参考に自分でポリシーを作成しました。不要そうなサービスへの権限を削除し、これでは権限が足りずエラーが出てしまったので、"cognito-idp:UntagResource"を追加しています。

※↓クリックするとIAMポリシーが見れます。

IAMポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "amplify:CreateApp",
                "amplify:CreateBackendEnvironment",
                "amplify:CreateBranch",
                "amplify:DeleteApp",
                "amplify:DeleteBackendEnvironment",
                "amplify:DeleteBranch",
                "amplify:GetApp",
                "amplify:GetBackendEnvironment",
                "amplify:ListApps",
                "amplify:ListBackendEnvironments",
                "amplify:ListBranches",
                "amplify:ListDomainAssociations",
                "amplify:UpdateApp",
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:CreateStackSet",
                "cloudformation:DeleteStack",
                "cloudformation:DeleteStackSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStackResource",
                "cloudformation:DescribeStackResources",
                "cloudformation:DescribeStackSet",
                "cloudformation:DescribeStackSetOperation",
                "cloudformation:DescribeStacks",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:GetTemplate",
                "cloudformation:UpdateStack",
                "cloudformation:UpdateStackSet",
                "cognito-identity:CreateIdentityPool",
                "cognito-identity:DeleteIdentityPool",
                "cognito-identity:DescribeIdentity",
                "cognito-identity:DescribeIdentityPool",
                "cognito-identity:SetIdentityPoolRoles",
                "cognito-identity:GetIdentityPoolRoles",
                "cognito-identity:TagResource",
                "cognito-identity:UpdateIdentityPool",
                "cognito-idp:AdminAddUserToGroup",
                "cognito-idp:AdminCreateUser",
                "cognito-idp:CreateGroup",
                "cognito-idp:CreateUserPool",
                "cognito-idp:CreateUserPoolClient",
                "cognito-idp:DeleteGroup",
                "cognito-idp:DeleteUser",
                "cognito-idp:DeleteUserPool",
                "cognito-idp:DeleteUserPoolClient",
                "cognito-idp:DescribeUserPool",
                "cognito-idp:DescribeUserPoolClient",
                "cognito-idp:ListTagsForResource",
                "cognito-idp:ListUserPoolClients",
                "cognito-idp:UpdateUserPool",
                "cognito-idp:UpdateUserPoolClient",
                "cognito-idp:UntagResource",
                "iam:AttachRolePolicy",
                "iam:CreatePolicy",
                "iam:CreateRole",
                "iam:DeletePolicy",
                "iam:DeleteRole",
                "iam:DeleteRolePolicy",
                "iam:DetachRolePolicy",
                "iam:GetPolicy",
                "iam:GetRole",
                "iam:GetRolePolicy",
                "iam:GetUser",
                "iam:ListPolicyVersions",
                "iam:PassRole",
                "iam:PutRolePolicy",
                "iam:UpdateRole",
                "lambda:AddLayerVersionPermission",
                "lambda:AddPermission",
                "lambda:CreateEventSourceMapping",
                "lambda:CreateFunction",
                "lambda:DeleteEventSourceMapping",
                "lambda:DeleteFunction",
                "lambda:DeleteLayerVersion",
                "lambda:GetEventSourceMapping",
                "lambda:GetFunction",
                "lambda:GetFunctionConfiguration",
                "lambda:GetLayerVersion",
                "lambda:GetLayerVersionByArn",
                "lambda:InvokeAsync",
                "lambda:InvokeFunction",
                "lambda:ListEventSourceMappings",
                "lambda:ListLayerVersions",
                "lambda:PublishLayerVersion",
                "lambda:RemoveLayerVersionPermission",
                "lambda:RemovePermission",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateFunctionConfiguration",
                "s3:CreateBucket",
                "s3:DeleteBucket",
                "s3:DeleteBucketPolicy",
                "s3:DeleteBucketWebsite",
                "s3:DeleteObject",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:HeadBucket",
                "s3:ListAllMyBuckets",
                "s3:ListBucket",
                "s3:PutBucketAcl",
                "s3:PutBucketCORS",
                "s3:PutBucketNotification",
                "s3:PutBucketPolicy",
                "s3:PutBucketWebsite",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "*"
        }
    ]
}

これより少なくすることはできるかもしれないですが、この権限があれば今回のCognitoとAmplifyでユーザー認証を構築するのは問題なくできました!

4. Amplifyの初期設定

Amplifyの設定を行うために下記のコマンドを実行します。

$ amplify init

聞かれた通りにプロジェクト名や使っているエディターなどを入力、選択していきます。プロジェクト名や環境名は、AWS上に作成されるリソースの名前になります。

? Enter a name for the project qiitatest
? Enter a name for the environment dev
? Choose your default editor: IntelliJ IDEA
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  yarn build
? Start Command: yarn start

そして、使うプロファイルについて聞かれるのでyと答え、先ほど3. Amplify用のIAMユーザーの作成で作成、保存したプロファイルamplify-cognitoを選択します。

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use amplify-cognito

この設定が完了すると、プロジェクト内にamplifyフォルダsrc/aws-exports.jsが作成されます。

おわりに

Amplifyフレームワークの設定が完了しました!Amplify用のIAMユーザーの作成方法(ポリシーの内容は変わってきます)やAmplifyの初期設定についてはCognitoに限らず他のサービスでも共通です。
次の構築完成編では、Cognitoユーザープールの作成とソースコードも書いていきます!

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

【React】ユーザー認証をCognito + Amplifyで構築してみた ~ 構築完成編 ~

はじめに

Reactで作成したWebアプリケーションのユーザー認証部分をCognito + Amplifyフレームワークで構築してみました。
本記事では、Cognitoのユーザープールの作成方法、Reactでのコード実装についてまとめています。

完成画面

今回は、ユーザー名、パスワードとワンタイムパスワードの2段階認証を行います。そして、サインインをしたユーザーを識別し、トップページには「{ユーザー名}さんこんにちは!」という文字が出るようにします。

ezgif.com-gif-maker.gif

方法検討

要件

認証方法を考えるにあたり、条件は以下の通りです。

  • 静的コンテンツをS3に置いている
  • アプリケーション部分はLambdaで実装している
  • アプリケーション内でユーザーを管理しており、ユーザーに登録されている人だけ使えるようにしたい
  • 今後ユーザーのマイページも作りたいので、認証と合わせてユーザーを識別したい
  • パスワードはシステム管理者が管理しないようにするのがベター
  • ソーシャルアカウントは使わない
  • MFA(多要素認証)を使いたい

結論

この条件に沿って認証部分を考えた結果、Cognitoを利用することにしました。また、Cognitoと既存のWebアプリケーションを簡単に連携できるAWSのAmplifyフレームワークというものがあったので、こちらも使ってみることにしました。

手順

下記の流れで進めていきます。

下記の1番から4番については別記事 構築準備編 へ。
1. Reactの開発環境の構築
2. Amplifyの設定
3. Amplify用のIAMユーザーの作成
4. Amplifyの初期設定

本記事ではこちらの手順についてまとめています。
5. Cognitoユーザープールの作成
6. コード実装

やってみる

5. Cognitoユーザープールの作成

前述の要件に沿ってユーザープールを作成していきます。
初めてのCognitoで何の設定をしているのかを調べるのが大変だったので1つ1つ残しておこうと思います。

Cognitoの作成を行うために下記のコマンドを実行します。

$ amplify add auth

詳細な設定を行うか、デフォルトの設定で良いか聞かれます。今回はMFAを使いたかったのでManual configurationを選択しました。

# Do you want to use the default authentication and security configuration?
Manual configuration

今回はサインインが使えれば良かったので、User Sign-Up & Sign-In only (Best used with a cloud API only)を選択しました。

# Select the authentication/authorization services that you want to use:
User Sign-Up & Sign-In only (Best used with a cloud API only)

リソース名とユーザープール名を入力します。

# Please provide a friendly name for your resource that will be used to label this category in the project: 
test
# Please provide a name for your user pool: 
test-cognito-mfa

ユーザー名とパスワードでサインインしたいので、Usernameを選択します。この項目は、後から変更できません。

# How do you want users to be able to sign in?
Username

参考
スクリーンショット 2020-11-01 14.47.16.png

今回はグループは作成しないのでNoを選択します。

# Do you want to add User Pool Groups? 
No

ユーザープール内の情報を取得したり、ユーザーを登録したりするAPIを叩けるようにするかを選択します。今回は使わないのでNoを選択します。
参考:https://docs.amplify.aws/cli/auth/admin

# Do you want to add an admin queries API?
No

今回はアプリケーション内でユーザーを管理していたため、このAPIを使わずに、自分でLambdaを作成しAWSのSDKを使ってユーザーの登録などを実装しましたが、このAPI使うのでも良かったかもしれないです。

MFAの設定を行います。今回はMFAを使いたいのでONを選択します。この設定はあとで変えられないので気をつけましょう。そして、MFAにはAuthenticatorやAuthyなどのアプリケーションを用いてのワンタイムパスワードを使いたいので、TOTPのみを選択しました。

# Multifactor authentication (MFA) user login options:
 ON (Required for all logins, can not be enabled later)
# For user login, select the MFA types: 
 Time-Based One-Time Password (TOTP)

参考
スクリーンショット 2020-11-03 19.28.34.png

SMSのメッセージの設定です。
SMSは使わず、Eメールを使うつもりなので、デフォルトのままEnterします。

# Please specify an SMS authentication message: 
 Your authentication code is {####}

ユーザープールにはEメールアドレスも登録し、Eメールを使ってユーザーの登録やパスワードの再設定を行いたいのでEnabledを選択します。

# Email based user registration/forgot password:
Enabled (Requires per-user email entry at registration)

参考(左メニュー [MFAそして確認] 内)
スクリーンショット 2020-11-03 23.37.46.png

認証コードを送るEメールの題名と本文の設定です。デフォルトのまま進みます。

# Please specify an email verification subject:
Your verification code
# Please specify an email verification message:
Your verification code is {####}

参考(左メニュー [メッセージのカスタマイズ] 内)
スクリーンショット 2020-11-06 23.10.00.png

パスワードポリシーを設定します。
文字数を8文字以上、数字、特殊文字、大文字、小文字を含むパスワードのみしか設定できないようにしました。

# Do you want to override the default password policy for this User Pool?
Yes
# Enter the minimum password length for this User Pool:
8
# Select the password character requirements for your userpool:
Requires Lowercase, Requires Uppercase, Requires Numbers, Requires Symbols

参考
スクリーンショット 2020-11-01 14.14.27.png

ちなみに、Do you want to override the default password policy for this User Pool?No (= デフォルトのパスワード設定)とすると、8文字以上という条件しか設定されません。

サインアップ時に必要な属性を設定します。パスワードの再設定等にメールアドレスを使用するため、Emailを選択しました。この項目は、後から変更できません。

# What attributes are required for signing up?
Email

参考(左メニュー [属性] 内)
スクリーンショット 2020-11-01 14.50.07.png

リフレッシュトークンはひとまずデフォルトの30日にしておきます。

# Specify the app's refresh token expiration period (in days): 
30

参考
スクリーンショット 2020-11-01 14.35.36.png

サインアップはユーザー自身が行わず別の処理で行うので書き込み権限は付与しません。また、ログインしたユーザーの識別にはUsernameを使うので読み取り権限も付与しません。

# Do you want to specify the user attributes this app can read and write?
No

今回は、アプリケーション内で事前に登録しているユーザーのみしかログインできず、ユーザー自身がサインアップすることがないので追加しませんでしたが、下記のような機能を追加することもできます。

  • Add Google reCaptcha Challenge
  • Email Verification Link with Redirect
  • Add User to Group
  • Email Domain Filtering (blacklist)
  • Email Domain Filtering (whitelist)
  • Custom Auth Challenge Flow (basic scaffolding - not for production)
# Do you want to enable any of the following capabilities? 
(選択なし)

今回はソーシャルログインは使わないのでOAuthはNoを選択します。

# Do you want to use an OAuth flow?
No

CognitoをLambda関数のトリガーとして使う予定はなかったのでnを入力。
例えば、ユーザーがサインアップしたことをトリガーとしてLambda関数を実行したいときなどに利用できます。

# Do you want to configure Lambda Triggers for Cognito?
No

参考
スクリーンショット 2020-11-01 14.22.29.png

これで構築するCognitoの設定が完了です。
そして、下記のコマンドで設定を保存します。

$ amplify push

環境やリソースがあっているかを聞かれるので、確認してyを入力します。

# ? Are you sure you want to continue?
Yes

これでCognitoのユーザープールが作成されました!そしてsrc/aws-exports.jsにはid等が書き込まれます。

最後に、このままだとユーザー自身がサインアップできてしまうので、Cognitoコンソールからできないようにします。
先ほど作成したCognitoのユーザープールを開き、左のメニューから[ポリシー]をクリックし、[管理者のみにユーザーの作成を許可する]を選択します。その後、[変更の保存]をクリックし、保存してください。

スクリーンショット 2020-11-03 23.27.48.png

Cognitoのコンソールからも設定を変更することもできますが、一部コンソールの変更では反映されないというのも見かけましたので、ご注意ください。Amplify CLIで設定を変更する場合は、$ amplify update authで設定を変更し、$ amplify pushを実行することで変更が保存されます。

6. コード実装

App.jsの中身を書き換えます。
参考:https://docs.amplify.aws/lib/auth/getting-started/q/platform/js

App.js
import React, {useEffect} from "react";
import Amplify, {Auth} from 'aws-amplify';
import awsconfig from './aws-exports';
import {withAuthenticator} from "@aws-amplify/ui-react";

Amplify.configure(awsconfig);

function App() {
    const [currentUserName, setCurrentUserName] = React.useState("");
    useEffect(() => {
        const init = async() => {
            const currentUser = await Auth.currentAuthenticatedUser();
            setCurrentUserName(currentUser.username);
        }
        init()
    }, []);

    const signOut = async() => {
        try {
            await Auth.signOut();
        } catch (error) {
            console.log('error signing out: ', error);
        }
        document.location.reload();
    }

    return (
        <div>
            <h1>{currentUserName}さんこんにちは</h1>
            <button onClick={signOut}>サインアウト</button>
        </div>
    );
}

export default withAuthenticator(App);

実行結果

ユーザーの追加

ユーザーの追加を行います。まだ、アプリケーション内から登録ができるようになっていないので、今回はAWSのCognitoコンソールから追加を行います。

先ほど作成したユーザープール内の左メニュー[ユーザーとグループ]を選択し、[ユーザーの作成]をクリックします。

スクリーンショット 2020-11-07 16.37.01.png

ユーザー名、ユーザーへの招待はEメールで行いたいのでEメールにチェック、電話番号は登録しないので検証済みのチェックを外し、Eメールアドレスを入力します。そして、[ユーザーの作成]をクリックし、ユーザーを作成します。ここで仮パスワードを未入力で登録すると、ユーザーにはランダムでパスワードが発行されます。

スクリーンショット 2020-11-07 16.43.13.png

ユーザーが作成できました。

スクリーンショット 2020-11-07 16.43.58.png

これでユーザーの登録は完了です。そしてユーザーには次のようなメールが送られます。

スクリーンショット 2020-11-07 16.44.59.png

サインイン一連の流れ(2回目以降)

冒頭の完成画面をご覧ください。
※初回サインイン時には、パスワードの変更、TOTP用のQRコードを読み取る操作が加わります。

サインインページからアカウントは本当につくれないのか

最後にAWSコンソールからユーザーがサインアップできないように設定しましたが、本当にできないのかを試してみます。

スクリーンショット 2020-11-07 18.01.02.png

エラーが出てサインイン画面からは登録できないことが確認できました!

おわりに

こんなに簡単にユーザー認証をつけることができました!画面もAmplifyのUIコンポーネントを使えば、こんなにも簡単に作成することができるんです!
ただ、このままではサインインページに使えないアカウント作成ボタンがあるので、そのボタンをなくしたい、AWSコンソールからではなくアプリケーションでユーザー管理できるようにしたいなど、要件に合うように多少カスタマイズも必要です。そのカスタマイズについて今後記事に残していければと思います。

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

styled-componentsの機能を用途に合わせて解説

はじめに

前回のreact-hook-formに続き、今回もreactの便利なパッケージのstyled-componentsについてまとめてみました!
自分もよく使うので使うときの参考にしたいと思ってます。

参考:styled-componentsドキュメント

styled-componentsとは

ReactにおけるCSSの当て方の1つで、Reactは他にもCSSModulesやCSSinJSなど色々なCSSの当て方がありますが、その中でも人気があり、個人的に一番気に入っています。特徴として、スタイル付きのコンポーネントを作ることができ、それに値を渡すことができます。

インストール

今回はTypeScriptを使うので、型定義ファイルもインストールします。

## npm
npm install --save-dev styled-components @types/styled-components

## yarn
yarn add -D styled-components @types/styled-components

通常のCSSと同じような使い方

まずstyled-componentsから、styledをインポートします。
基本的な使い方として、styledの後にタグ名を書いて、その中で通常のscssのような感じで記述できます。

下の例では、h1タグにスタイルが当たった物をTitleとして使用しています。
その中のspanにもスタイルを当てたり、擬似要素を使ったりしています。

import React from "react";
import styled from "styled-components";

export const App = () => {
  return (
    <Title>
      hello<span>world!</span>
    </Title>
  );
};

const Title = styled.h1`
  font-weight: bold;
  span {
    color: red;
  }
  &:hover {
    color: blue;
  }
`;
通常 hover

コンポーネントのスタイルを拡張

自分の作成したコンポーネントやUIライブラリのコンポーネントにスタイルを当てたい時は、コンポーネント名を()で囲うように記述します。
以下の例では、上で作成したTitleを拡張しているのでfont-weight: boldになっていて、colorはgreenに上書きされています。

const StyledTitle = styled(Title)`
    color: green;
`;

image.png

UIライブラリによって、スタイルの優先度が高かったりして、思った通りにスタイルが当たらないことがあります。
(ドキュメントに載ってたり、調べたら大体すぐ出てくるので調べてみてください。)

他のタグでも使用する

上の方法だけだと、Titleのスタイルがh1タグとしてしか使えないので、divタグやpタグなど他のタグとしても使いたい時は、asを使います。
以下の例では作成したTitleをh1タグではなく、divタグとして使用しています。

    <Title as="div">
      hello<span>world!</span>
    </Title>

image.png

同じような機能として、withComponentというのがありますが、asの方が簡潔だからか非推奨になっているみたいです。

一部のスタイルを使いまわしたい

コンポーネントとしてではなく、スタイルを色々なところで使いたい時はcssを名前インポートして使います。

下のような感じで、cssを使ってスタイルを作成し、他のスタイルに埋め込むことができます。また、変数や定数を使うこともでき、スタイルを統一するのに役立ちます。

const justifyContentCenter = css`
  display: flex;
  justify-content: center;
`;

const Container = styled.div`
  background-color: ${COLOR.WHITE};
  ${justifyContentCenter}
`;

const Box = styled.div`
    margin: 20px;
    ${justifyContentCenter}
`;

1、2個スタイルを当てる時にわざわざコンポーネントを作りたくない

csspropというのがあり、下のように記述できるのですが導入がやや面倒です。とても長くなってしまうので導入方法は割愛します。csspropはbabelを使って、classを作ってくれるのでインラインスタイルの優先度の問題は解決できますが、jsx内に多く記述すると見辛くなるので、1、2個程度で使う感じになると思います。

こちらの記事がとても参考になります!
styled-componentsのcss propを、create-react-appで作成したReact & TypeScriptの環境で動かすためのチュートリアル

    <h1 css={`color: white`}>
      hello world!
    </h1>

値を渡して使う

styled-componentsと言ったらこれ!みたいな感じがあります。下のように親コンポーネントから子コンポーネントにpropsを渡すのと同じようにして、値を渡すことができます。

import React from "react";
import styled from "styled-components";

export const App = () => {
  return (
    <Title color="red">
      hello<span>world!</span>
    </Title>
  );
};

const Title = styled.h1<{ color: string }>`
  color: ${(props) => props.color};
`;

この例の場合、color='red'が渡されて、その値を下で参照しているのでcolor: redになります。

複数の値を渡したり、分割代入で記述を減らしたりもできます

import React from "react";
import styled from "styled-components";

export const App = () => {
  return (
    <Title color="red" background="black">
      hello<span>world!</span>
    </Title>
  );
};

const Title = styled.h1<{ color: string; background: string }>`
  color: ${({ color }) => color};
  background-color: ${({ background }) => background};
`;

終わりに

ここまで読んでいただきありがとうございました!少しでもstyled-componentsについて、理解が深まれば嬉しいです。
他にもattrsthemeなどの機能がありますが、今のところあまり使用していない&有効的な使い方がわかっていないので今回は書きませんでした。興味がある方は調べてみてください。もし良い使い方があったりしたら教えていただけると嬉しいです。
他にも記事投稿しているので良ければ見てください!

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

React 未学習者 Q.「React って学習コスト高いんでしょ?」A.「

いやいやいやいや!高くない高くない、全く高くない!
簡単!React なんてホント簡単だから!!超簡単だよ?
ほら、これみて!

<div id='root'>Hello world</div>

これ React で表示するなら、

ReactDOM.render(
  <div id='root'>Hello world</div>,
  document.getElementById('root')
);

ってするだけだからね?
ReactDOM.render([描画したい要素], [描画する位置]) って書いてるだけだからね?

Q.「コンポーネントって難しいんでしょ?」A.「

いやいやいやいや!難しくない難しくない、全く難しくないって!
簡単!コンポーネントなんてホント簡単だから!超簡単だよ?
ほら、これみて!

const App = () => {
  return <div id='root'>Hello world</div>;
}

これ App って名前付けたコンポーネントね。
コンポーネントなんて単に関数みたいなの書くだけだからね?
コンポーネント名の 1 文字目を大文字にして、描画したい要素を return するだけだからね?

え、App コンポーネントを描画したい?それなら、

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

ってするだけだからね?ほら、簡単でしょ?

Q.「Props っていう新しい考え方を覚えないといけないんでしょ?」A.「

いやいやいやいや!新しくない新しくない、全く新しくないって!
データデータ!Props なんて単なる受け渡し用のデータだから!
ほら、これみて!

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

これ、さっきの App コンポーネントね。これに title っていう名前で PROPS って値を渡したいときは、

ReactDOM.render(
  <App title='PROPS'/>,
  document.getElementById('root')
);

って書くわけ。この追記した部分 title='PROPS' を Props って読んでるだけだからね?
ちなみに受け取りは、App コンポーネントで

const App = (props) => {
  return <div id='root'>Hello {props.title}</div>; // 'Hello PROPS'
}

ってするだけだからね?ほら、簡単でしょ?

Q.「State っていうのも聞いたことがあるんだけど?」A.「

そうそうそうそう!State 、State !なんで State 知ってるの?神なの?
State はデータ!State は内部管理用のデータだから!
ほら、これみて!

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

これ count っていう名前の State と、State を更新する setCount って関数を記述してるだけだからね?

useState(0) って単に count の初期値を 0 ってしてるだけだからね?

え、つまり State はどう使うのって?
例えば、

const {useState} = React; // state 使う時はこれ書いてね。

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>Click me</div>
  );
}

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

みたいにすれば、クリックすると 1 増える div 要素が作れるわけ。ほら、簡単でしょ?

Q.「・・・・・」A.「

あ、そういえば、いままで言ったことって 1 つの HTML ファイルで書く想定だったから、書く時は次の script タグ加えてね。(重要)

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

あと 自分で書くソース部分は <script type="text/babel"> ... </script> って書いてね。(重要)

あとがき

こんな感じで、全力&前のめりに React の学習は簡単だよって言ってくれる人がいたら良かったのに、と思ったので書きました。

JSX とかクラスコンポーネントとか環境構築の話とかはあえて触れずに書いてます。新しいこと学ぶときってどうしても心理的ハードルが生まれると思うので、覚えることは最小限にして伝えたくて。

この記事が誰かの React 学習のとっかかりになれると嬉しいです。

読んでいただき、ありがとうございました。

みんな React やろうぜ :)


Kindle(セルフ出版)で本出してます。ご興味ある方はぜひ。

React学習挫折経験者による挫折しないReact基礎学習

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

React 環境構築をしてみよう!

Homebrew をインストール

まずcreate-react-appをインストールするため、Homebrewをインストールします。以下のコードをターミナルに打ち込みましょう。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

次にインストールできたか確認します。

brew -v

バージョンが出てきたらインストール完了。

nodebrew をインストール

brew install nodebrew

数分待ちます。そして確認します。

nodebrew -v

node をインストール

nodebrew install stable

インストールしたバージョンの確認をします。

nodebrew ls

インストールしたバージョンが出てくるので、覚えときましょう。currentがnoneになっていると思うので、バーいジョンを指定してあげます。

nodebrew use インストールされたバージョンを入力(例 v12.16.1)

次にnodeを使うためパスを入力します。

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile

nodeのバージョンを確認。

node -v

npm 入っているか確認します。

npm -v

開発したいファイルに移動します。

cd (例 Projects)

移動したらreactのインストールです。

npx create-react-app (つくりたいファイル名)

インストールしたら、エディタで開きターミナルで

npm run build

インストール終わったら

npm start

をして環境構築完了です!!

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

ReduxToolkitのインストール手順まとめ

この記事について

  • 最近React + ReduxToolkitを使用しての開発案件を行ったのでフレームワーク別に手順を整理
  • ReduxToolkitとは?的な話は割愛(魅力を語る記事は検索するとたくさん出てくるので)

create-react-appを使用する場合

新規プロジェクト作成と同時にインストールする場合

それぞれ指定したtemplateはデフォルトでReduxToolkitがインストールされます

  • JavaScriptの場合
  create-react-app {プロジェクト名} --template redux
  • TypeScriptの場合
  create-react-app {プロジェクト名} --template redux-typescript

既存プロジェクトへ追加する場合

redux本体はReduxToolkit(@reduxjs/toolkit)に含まれているので、明示的に指定してインストールは不要です

  • JavaScriptの場合
  yarn add @reduxjs/toolkit react-redux
  • TypeScriptの場合
  yarn add @reduxjs/toolkit react-redux
  yarn add -D @types/react-redux

create-next-app(Next.js)を使用する場合

新規プロジェクト作成と同時にインストールする場合

公式githubに色んなテンプレートがあるのでそれを指定

  • JavaScriptの場合
  create-next-app --example with-redux-toolkit {プロジェクト名}
  • TypeScriptの場合

    create-react-appの時みたいにReduxToolkit + Typescriptの都合の良いテンプレートはなさそうなので、何かと設定に手間がかかりそうなTypeScriptのテンプレートを指定してプロジェクト作成して後から個別でReduxToolkitをインストールします。

  create-next-app --example with-typescript {プロジェクト名}

既存プロジェクトへ追加する場合

create-react-appの所に書いた方法と同じ

今回使用したNext.jsのテンプレート

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