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

React*Material-UIのCreateStylesで擬似クラスを扱う

概要

何故かMaterial-UIのCreateStyles環境下における擬似クラスの取り扱いの記述が公式にあまりなかった為、どうやって適用すれば良いのか当初詰まりました。

最終的にこうやれば良いのだと発見した為、確認できた対応方法を記載致します

記載方法

sample.tsx
const useStyle = makeStyles(theme =>
  createStyles({
    box1: {
      color: 'black',
      '&:focus': {
        '& + $box2': {
          color: 'red',
        },
      },
    },
    box2: {
      color: 'black',
    },
  }),
);

export function InputStandard() {
  const clsx = useStyle();

  return (
    <div>
      <input className={clsx.box1} />
      <div className={clsx.box2}>box2</div>
    </div>
  );
}

CreateStylesの場合は通常のCSSの記載方法が取れず、例えば隣接要素へfocus時のCSSを適用したい場合は、focus対象のクラス内でネストさせて、そこで $class名とする必要がありました。

なおこの例では$class名としておりますが、$要素名でも行けます。
そもそもStateで管理してJS側で管理するパターンの方が多いかもしれませんが、どなたかの参考になれば幸いです

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

Reactについての知見まとめ

useStateの状態を変更したときは再レンダリングが行われますが、useRefの状態を変更したときは行われません。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState<number>(0);
  console.log(count); // 0 と 1 が出力されます
  useEffect(() => {
    setCount(1);
  }, []);
  return <></>;
};
const App: React.FC<{}> = () => {
  const count = useRef<number>(0);
  console.log(count.current); // 0 だけが出力されます
  useEffect(() => {
    count.current = 1;
  }, []);
  return <></>;
};


useStateで今の状態から次の状態を作るとき、次のように実装すると意図した結果が得られません。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
  }
  return <>
    {/* 1 クリックにつき count は 1 しか増えません */}
    <button onClick={handleClick}>Click me!</button>
    {count}
  </>;
};

意図した結果を得るためには、setCountに関数を渡します。この関数の第一引数には今の状態が渡ります。関数の戻り値が次の状態になります。

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count => count + 1);
    setCount(count => count + 1);
  }
  return <>
    {/* 1 クリックにつき count は 2 増える */}
    <button onClick={handleClick}>Click me!</button>
    {count}
  </>;
};


useEffectに渡した関数が実行されるタイミングは、レンダリングが完了した後です。よって、次のコードは必ずレンダリング後の要素の幅と高さを出力します。

const App: React.FC<{}> = () => {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current === null) return;
    const style = window.getComputedStyle(ref.current);
    console.log(style.width, style.height); //=> 1440px 18px
  }, []);
  return <div ref={ref}>test</div>;
};


useRefcurrentの変更を検知したいことがあります。しかし、useRefにはcurrentの値の変更を検知する方法は用意されていません。変更を検知したいときは、代わりに「コールバックref」を使います。コールバックrefの具体例としては、canvas要素が存在するときに限りコンテキストを取得したい、というケースがあります。

次のコードは、canvas要素が存在すればsetContextを実行し、存在しなければ何もしないコードです。

const App: React.FC<{}> = () => {
  const [show, setShow] = useState(true);
  const [, setContext] = useState<CanvasRenderingContext2D | null>(null);
  useEffect(() => {
    // 1秒毎にcanvas要素の存在の有無を切り替えます
    const id = window.setInterval(() => setShow(show => !show), 1000);
    return () => window.clearInterval(id);
  }, []);
  // useCallbackに渡した関数は、nodeが変わったときに呼ばれます
  const callbackRef = useCallback((node: HTMLCanvasElement | null) => {
    if (node === null) return;
    setContext(node.getContext('2d'));
  }, []);
  return <>
    {show && <canvas ref={callbackRef}></canvas>}
  </>;
};

同等のことをコールバックrefなしで実現しようとすると、おそらく複雑なコードになります。


useStatesetXXXを実行した後のレンダリングのタイミングは遅延されることがあります。たとえば次のコードは、setCount(1)実行後に即座にレンダリングされず、setCount(2)実行後にレンダリングされます。よってコンソールには02が出力されます。

const Child: React.FC<{ count: number }> = ({ count }) => {
  console.log(count);
  return <></>;
};

const App: React.FC<{}> = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    (async () => {
      // await null;
      setCount(1);
      setCount(2);
    })();
  }, []);
  return <Child count={count} />;
};

しかし、// await null;のコメントアウトを外すだけで、コンソールには012が出力されるようになります。これは、setCount(1)実行後に即座にレンダリングされることを意味します。

このように、setXXX実行後にレンダリングされることは保障されていますが、実行後のどのタイミングでレンダリングされるかについては保障されていません。したがって、タイミングへの依存はなくす必要があります。

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

Twitter APIで取得できるツイートのIDの丸め誤差問題を解決する

現在React Native + Expo + Twitter APIで個人開発をしているのですが、Twitter APIに苦戦したので書きます。
コードは基本的にReact/React Native上で動くものになります。

idが大きすぎる

Twitter APIでは一度で取得できるツイート数に制限があります。

import Client from '../hoge';

const getTweets = () => {
  Client.get('favorites/list.json',{
    count: 200, // 最大200まで
    tweet_mode: 'extended',
  })
};

なので、自動スクロールなどで追加取得する際にパラメーターにmax_idかsince_idを追加する必要があります。

import Client from '../hoge';

const getTweets = () => {
  Client.get('favorites/list.json', {
    count: 200,
    max_id, // 追加
    tweet_mode: 'extended',
  })
};

この時、最後に取得したツイートのidをそのまま渡すと、渡されたID以下のツイートが取得されることになるので、最後のツイートが重複してしまいます。なので、idに-1をしてから渡さないといけません(古い順で取得してる時は+1)。

ここで問題が発生します。これは実際の自分のツイートのレスポンスです。

{
  created_at: "Sat Feb 20 13:38:04 +0000 2021",
  id: 1363120740321558500,
  id_str: "1363120740321558529",
  full_text: "明日中にアプリの申請絶対だすぞ",
  ...
}

number型のidとstring型のid_strが返ってくるのですが、number型の方は値が大きすぎて丸まっています。さすがTwitterです。

実際に計算をしてみても、結果は以下のようになります。

console.log(tweet.id, tweet.id - 1);
// 1363120740321558500 1363120740321558500

正確な値であるid_strの方を使うことになるのですが、文字列なので数値に変換しないといけません。しかしparseIntなどで変換しても上記の計算と同じ結果になってしまうので、Big Intとして計算しないといけません。

解決策

簡易的なコードの例ですが、自分の解決策を載せます。bignumber.jsというライブラリを使用しました。

yarn add bignumber.js
import React, { useState } from "react";
import { BigNumber } from "bignumber.js";
import Client from '../hoge';

export const App = () => {
  const [tweets,setTweets] = useState([]); // 既に取得しているツイートが入ります

  ...

  const getMoreTweets = () => {
    const max_id = new BigNumber(tweets.slice(-1)[0].id_str).minus(1).c?.join(''); // 最後に取得したツイートのidを-1
    Client.get('favorites/list.json', {
      count:200,
      max_id,
      tweet_mode: 'extended'
    }).then((res:any) => setTweets([...tweets,...res]));
  }

  ...

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

framer-motionを使って遷移時にアニメーションを追加する

お題

framer-motionライブラリの使い方

・framerが提供しているコンポーネントにアニメーションを付与することができる。

・タグでJSXを包むことでページ遷移にもアニメーションをつけることができる。

導入方法

①npm install framer-motionでインストールする

②AppRouterページでswitchタグをタグとタグで囲む

包んだswitchにはlocationとkeyを設定する。locationはどこのページにいるかframer-motionに教える役割を持っており、これが設定されていないとアニメーションが機能しない。keyにはlocation.pathnameを設定。これはパス名。ここではloginやtitleが該当する。

HashRouterを使っている場合はRoute render={({ location }) => ()}で取得することができる。

AppRouter.js
import { AnimatePresence, motion } from "framer-motion"
  <Router>
    <Route render={({ location }) => (
        <AnimatePresence exitBeforeEnter initial={false}>
            <motion.div>
            <Switch location={location} key={location.pathname}>
                <Route exact path="/" render={() => <Redirect to="/top" />} />
                <Route exact path="/login" component={LoginPage} />
                <Route exact path="/title" component={TitlePage} />
            </Switch>
            </motion.div>
        </AnimatePresence>
        )}
    />
  </Router>

これで下準備は完了。後はアニメーションを設定したいページにmotion.divを入れこみます。

ログインページもといサンプルページで実装するとこんな感じになります。

Login.js
import { motion } from "framer-motion";
import { animationService } from '../../services/AnimationService';

class LoginPage extends Component {

  constructor(props) {
    super(props);
    const errors = {
      mail: "",
      password: "",
    };
    this.state = {
      animation: animationService.getAnimationMotions()
    }
  }
  render() {
    return (
      <Page>
        <LoginHeader title="ログイン" />
        <div id="login-page">
          <motion.div 
            animate={{x: 0}}
            initial={{x: 100}}
            exit={{x: -100}}
            transition={{duration: 0.2}}>
            <Button modifier="large" onClick={this.submit} disabled={this.state.isBtnDisabled}>ログイン</Button>
          </motion.div>
        </div>
      </Page>
    );
  }
}

export default withRouter(LoginPage);

motion.divのプロパティにそれぞれanimate,initial,exit,transitionと設定しています。遷移アニメーションで重要なのは始発点(initial)と終着点(exit)であり、これをinitialとexitでそれぞれx地点のどこからアニメーションが始まるかまたは終了するかを設定する必要があります。

transitionにはアニメーションが動作する時間を設定します。

以上で基本的な実装は完了。

他にもコンポーネントそのものにアニメーションを加えたりページによって遷移アニメーションを分けることもできるので組み合わせ次第でよりリッチに見えるアプリを作ることができます。

initialとかexitの値をアプリで固定して使いたい場合は別ファイルでまとめて管理すると楽になります。

まとめ

react.jsでアニメーションを描画するのってめんどくさそうと思っていましたがこのライブラリはAppRouterを多少いじることに目を瞑ればめちゃめちゃ便利ですね。

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

入力文字の行数で縦幅が可変するtextareaのReactコンポーネント

概要

次の記事を参考に入力文字の行数で縦幅が可変するReactコンポーネントを作成しました
react hook formがすぐに使いたかったのでforwordRefを使用して実装しています

TypeScriptのコード

import { ComponentProps, forwardRef, useRef } from "react"

export const FlexTextArea = forwardRef<HTMLTextAreaElement, ComponentProps<"textarea">>(
  (props, ref) => {
    const dummyRef = useRef<HTMLDivElement>(null)

    return (
      <div className={"flex_text_area"}>
        <div className={"flex_text_area_dummy"} aria-hidden={true} ref={dummyRef} />
        <textarea
          {...props}
          ref={ref}
          onChange={(e) => {
            if (dummyRef.current) {
              dummyRef.current.textContent = e.target.value + "\u200b"
            }
            if (props.onChange) {
              props.onChange(e)
            }
          }}
        />
      </div>
    )
  },
)

スタイル(less)

.flex_text_area {
    position: relative;

    .flex_text_area_dummy {
        overflow: hidden;
        visibility: hidden;
        box-sizing: border-box;
        padding: 5px 15px;
        min-height: 120px;
        white-space: pre-wrap;
        word-wrap: break-word;
        overflow-wrap: break-word;
        border: 1px solid;
    }

    & > textarea {
        position: absolute;
        top: 0;
        left: 0;
        box-sizing: border-box;
        padding: 5px 15px;
        width: 100%;
        height: 100%;
        background-color: transparent;
        border: 1px solid #b6c3c6;
        border-radius: 4px;
        color: inherit;
        font: inherit;
        letter-spacing: inherit;
        resize: none;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習週次報告 #2

お疲れ様です。
この記事は、すごく個人的な学習週次報告です。
自分自身の「目標の明確化」「学んだこと・わからないことの整理」「成長記録」のために書いています。

今回は、”今週で主にやったこと”から、
”今週で学んだこと””つまづいたけど7、8割くらい理解できたこと”
”つまづいて、今でもよくわかっていないこと”を整理して、
”次週何をやるべきか”を書いていきます。


今週で主にやったこと

  • React、ReacrRauter、Redux、ReactRedux、ReduxThunkの復習
    • React:簡易的な掲示板の作成
    • ReacrRauter:APIを使ったクイズアプリの作成
    • Redux:TODOを管理できるツールの作成
    • ReactRedux:Reduxで作ったツールを元にTODOアプリの作成
    • ReduxThunk:ReacrRauterで作ったクイズアプリのデータをStore管理できるようにする
  • ”リーダブルコード”を読んだ
  • ”自己紹介サイト”の制作(まだ構成を形にした段階で、完成には至っていない)

今週で学んだこと

  • ”アトミックデザイン”について学んだ
    • ページ構成の最小単位(ボタンやフォームなど)からデザインを決めていき、最終段階のページ制作を効率化させるやり方のこと
    • 従来のデザインと違って、コーディング目線から産まれた手法
    • メリット1:更新変更に強い
    • メリット2:特に大規模プロジェクトでは、メンバーで共有しやすいため、一貫性を持たせられる

つまづいたけど7、8割くらい理解できたこと

  • React Reduxでのコンポーネントのコンテナー化はまだつまずくが、
    進めているうちに、Storeの情報をmapStateToPropsで取得してコンポーネントに持ってきたり、
    コンポーネントサイドで行ったStore情報の更新をmapDispatchToPropsで送ったりしてるのかなあ、と
    なんとなく感覚が掴めてきている感じがする。
  • サイト制作にあたってyarnインストールしてみた
    (理由:どうやらnpmより速いらしく、いろんなネット記事もyarnが多かった)
    が、なぜかエラー
    → sudoで解決した(参考記事:https://qiita.com/tsumita7/items/a40a367088018b5bbe33)

つまづいて、今でもよくわかっていないこと

  • ”リーダブルコード ”を一応全部読んでみたが、
    後半になるにつれて話は高度になっていき、自分のレベルではあまり理解できず。
    しかし、序盤は初心者にもすごくわかりやすく、綺麗なコードをかく意義とテクニックが書いてあり、
    最後の章には具体的な機能実装の手順があって、エンジニア思考のプロセスが追えて勉強になった。
    ただ、今の自分にとって緊急性のある内容ではないように感じたので、
    また、コーディングに慣れてきた頃に読み返そうと思う。

次週何をやるべきか

  • 感覚を定着させるためにまたReact、ReacrRauter、Redux、ReactRedux、ReduxThunkの復習
  • もう来週には完成をめどに、”自己紹介サイト”制作を進める

内容は以上です。
みなさんお互いに頑張りましょう。little by little.

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

[AWSxAmplifyxCognito] Amplifyでグローバルサインアウトがグローバルサインアウトしてくれない問題を解決する

はじめに

こんにちは!
認証機能を実装する場合、別のデバイスでサインアウトしたらユーザーがログインしている他の全てのデバイスでもログアウトして欲しいときがありますよね?

Amplifyではそんな要望に応えるために、singOut関数にglobalオプションがあり、signOut({global:true})のようにするとサインアウトした際に、ユーザーが持つ全トークンが無効化され、全てのデバイスからサインアウトできます。
以下ドキュメントから抜粋
image.png

と、いうのがドキュメントには書かれており、実際ユーザーが認証後に取得する3種類のトークンのうち、アクセストークンとリフレッシュトークンは即座に無効になるのですが、
実は、signOut({global:true})をしてもIDトークンは無効にならないんです!
加えて、セッションを確認するための関数たち(currentSession,userSession)はCognitoにアクセストークンやリフレッシュトークンが有効なのか問い合わせにいったりはせず、ローカルストレージにあるIdトークンの有効期限を確認するだけなんです。

つまり、currentSession,userSession関数では、1つのデバイスでグローバルサインアウトが行われていても、他のデバイスではIdトークンが有効期限切れにならない限り、ユーザーは"セッション切れ"とは判断されないんです。

対応策

currentSession,userSession関数では、アクセストークン、リフレッシュトークンが有効かどうか見てくれない。
なら、それらが有効かどうかを問い合わせる関数を使えばいいのですが、その確認のためだけの関数がAmplifyリファレンスに見当たらないんです。

なので、実行時にアクセストークンを使う関数を利用して、その有効・無効を判断します。
例えば、currentUserInfo関数がそれにあたります。

この関数は,

  • サインイン状態のとき、以下のようなusername,attributes(cognitoで設定したユーザーの属性)を含むオブジェクトが返ります。
{
    "UserAttributes": [
        {
            "Name": "sub",
            "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        },
        {
            "Name": "email_verified",
            "Value": "true"
        },
        {
            "Name": "email",
            "Value": "xxxxxxxxx@xxxxxxxxx.com"
        }
    ],
    "Username": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
}
  • そのデバイスでサインアウトして、デバイスにトークンがないときはnull
  • 別のデバイスでサインアウトして、そのデバイスに各種トークンはあるが、アクセストークンが無効なときは、空オブジェクト{}が返ります

したがって、返り値が null または 空オブジェクト のときユーザーはサインアウト状態であるといえます。

以下はサインイン画面での実装例です。

サインイン画面側の実装例

サインインしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト)の否定である nullでない かつ 空オブジェクトでない でサインイン状態かどうかを見ています。(空オブジェクトでない はプロパティがundefinedかどうかで見ています。)

const getCurrentUserInfo = async () => {
    const currentUserInfo = await Auth.currentUserInfo();
    return currentUserInfo;
}
getCurrentUserInfo()
    .then(currentUserInfo =>{
        //以下の場合はサインアウト状態
        //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき
        //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき
        if(currentUserInfo !== null && currentUserInfo.username !== undefined){
            //サインインしているときにしたい処理を書く。ログイン画面に飛ばすとか。
        }
    })
    .catch(err=> {
        console.log(err)
    })

サインイン後の画面の実装例

サインアウトしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト)でサインアウト状態かどうかを見ています。

const getCurrentUserInfo = async () => {
    const currentUserInfo = await Auth.currentUserInfo();
    return currentUserInfo
}
getCurrentUserInfo()
    .then(currentUserInfo =>{
        // 以下の場合はサインアウト状態なのでログイン画面に飛ばす。
        //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき
        //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき
        if(currentUserInfo === null || currentUserInfo.username === undefined){
            //ログイン画面に飛ばす処理
        }
    })
    .catch(err=> {
        console.log(err)
        //currentUserInfoが取得できないときはログイン画面に飛ばす。
        //ログイン画面に飛ばす処理
    })

以上です。
上記の処理をページの描画ごとに行えば、
Idトークンが有効であっても、
そのデバイスでサインアウトしている場合だけでなく、別のデバイスでサインアウトしているかどうかも判定ができます。

自分の方法が唯一解ではないと思うので、別案・改善案あればぜひ共有していただければ!

終わりに

公式の見解

実は、signOut({global:true})がドキュメント通りの挙動でない問題は現在openなissueとしてあがっています。
cognito.user.signOut() does not invalidate tokens

このissueは2019年6月にopenされて現在もまだcloseされていません。このissue以外にも同様の件に関するissueは何件か見られました。

issueにコメントしているユーザーの1人がコメントで個人的にAWSに問い合わせした際の返事を転記していて、それを信じるなら、AWS側はこの問題を認知していて現在取り組み中だそうです。。
image.png

以下のどちらかがいつか実装されるといいなと思います。できれば前者。

  • グローバルサインアウトでidトークンが即時無効になり、かつ session関数がローカルストレージのトークン有効期限ではなくcognitoに有効かどうかを確認しに行く
  • アクセストークンが有効かどうかをCognitoに問い合わせる関数が生える
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Redux の Storeを空にする

React, Redux, ReduxToolKitにかまけていたら、storeキャッシュの方法がsliceの範囲外だったため、それの備忘録

TL;DR

root reducerを設置して、storeにundefinedを設定する、

action.ts
import { createAction } from "@reduxjs/toolkit";

export const clearCacheBase = createAction('[CORE/STIORE] Clear Store Cache')

export const clearCache = () => {
    return clearCacheBase()
}
reducer.tsx
import { combineReducers, configureStore } from "@reduxjs/toolkit"
import * as fromTodo from "./todo"
import { clearCacheBase } from "./core/action"

const combinedReducer = combineReducers({
    [fromTodo.featureName]: fromTodo.reducer
})

export const reducers = (state, action) => {
    if(action.type === clearCacheBase.toString()) {
        state = undefined // undefinedを追加するとstoreが初期値に全て戻る
        // return combinedReducer(undefined, action)でも動く こっちの方が再代入なく直感的かも
    }
    return combinedReducer(state, action)
}

export type RootReducer = typeof reducers
component.tsx
import React from 'react';
import { useDispatch } from "react-redux"
import { clearCache } from "../store/core/action"

/* eslint-disable-next-line */
export interface ButtonProps {}

export function Button(props: ButtonProps) {
  const dispatch = useDispatch()
  return (
    <div>
      <button onClick={() => dispatch(clearCache())}>clear cache</button>
    </div>
  );
}

export default Button;

レポ diff

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

領域に収まらない子要素をflexで折り返すには?

背景

バックエンドからデータを取得し、そのデータをもとにカードを並べたい。
しかし、通常並べるだけでは以下のように無限に横並びになってしまう。

スクリーンショット 2021-02-20 16.02.46.png

解決策

折り返したい子要素を包む親要素に以下のCSSを指定する。

display: flex;
flex-wrapper: wrap;

解決画面

以下の通り、親領域に収まらない時は折り返されるようになった。

スクリーンショット 2021-02-20 16.02.10.png

参考コード

// 折り返したい子要素を包む親要素
const CardsWrapper = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 12px;
  flex-wrap: wrap; // flexでwrap指定する。
`;

// 折り返したい子要素
const CardHolder = styled.div`
  margin-right: 6px;
`;

const App = () => {
// ...

// 取得したデータ数だけカードをレンダリングする関数。
    const alignCards = (data) => {
      return (
        <CardsWrapper>
        {data.map((_data, i) => {
          return (
            <CardHolder><SampleCard name={data[i].title}/> </ CardHolder>
          )
        })}
        </CardsWrapper>
      )
  }
// ... 

  return (
    <>
      {alignCards(movieDataArraysObj)}
    </>
  )
}

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

領域に収まらない子要素をflexで折り返す

背景

バックエンドからデータを取得し、そのデータをもとにカードを並べたい。
しかし、通常並べるだけでは以下のように無限に横並びになってしまう。

スクリーンショット 2021-02-20 16.02.46.png

解決策

折り返したい子要素を包む親要素に以下のCSSを指定する。

display: flex;
flex-wrapper: wrap;

解決画面

以下の通り、親領域に収まらない時は折り返されるようになった。

スクリーンショット 2021-02-20 16.02.10.png

参考コード

// 折り返したい子要素を包む親要素
const CardsWrapper = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 12px;
  flex-wrap: wrap; // flexでwrap指定する。
`;

// 折り返したい子要素
const CardHolder = styled.div`
  margin-right: 6px;
`;

const App = () => {
// ...

// 取得したデータ数だけカードをレンダリングする関数。
    const alignCards = (data) => {
      return (
        <CardsWrapper>
        {data.map((_data, i) => {
          return (
            <CardHolder><SampleCard name={data[i].title}/> </ CardHolder>
          )
        })}
        </CardsWrapper>
      )
  }
// ... 

  return (
    <>
      {alignCards(movieDataArraysObj)}
    </>
  )
}

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

React Typescriptでcontainer単体テストでつまったはなし

概要

React Typescript, jestで単体テストの簡単な作り方。

Tl;DR

redux-mock-storeでmock-storeを作成。
jest.requireaActualでProvider呼び出し、useDispacthとuseSelectorをmock化する。

想定ファイル

todo.tsx
import { createDraftSafeSelector } from '@reduxjs/toolkit';
import React from 'react';
import { selectAllTodo } from '../store/todo/selector';
import { useselector } from "react-redux"

const containerSelector = createDraftSafeSelector(
  selectAllTodo,
  todos => ({
    todos
  })
)

/* eslint-disable-next-line */
export interface TodoListContainerProps {}

export function TodoListContainer(props: TodoListContainerProps) {
  const { todos } = useselector(containerSelector)
  return (
    <div>
      {todos.map(t => (<div key={t.title}>{t.title}</div>))}
    </div>
  );
}

export default TodoListContainer;

テストファイル

todo.spec.tsx
import React from 'react';
import { render } from '@testing-library/react';

import TodoListContainer from './todo-list-container';

describe('TodoListContainer', () => {
  it('should render successfully', () => {
    const { baseElement } = render(<TodoListContainer />);
    expect(baseElement).toBeTruthy();
  });
});

解決策として、redux-mock-storeとjestでモック化する

todo.spec.tsx
import React from 'react';
import { cleanup, render } from '@testing-library/react';
import configureStore from "redux-mock-store" // reudux mock storeでデータを読み込む
import TodoListContainer from './todo-list-container';

const mockStore = configureStore() // mockStore Functionを作成

// jest.mock('react-redux') で'react-redux'全体がMock化されるため、ProviderをActualで呼び出す。
const { Provider } = jest.requireActual("react-redux")

// mock化されたdispatcherとselectorを作成
// selectorの実装を変えたときについては、jestのmock functionを書き換える。
jest.mock("react-redux", () => ({
  useselector: jest.fn().mockReturnValue({
    todos: []
  }),
  useDispatch: jest.fn().mockReturnValue(jest.fn())
}))

describe('TodoListContainer', () => {
  afterEach(() => {
    cleanup()
  })
  it('should render successfully', () => {
    const { baseElement } = render(
      <Provider store={mockStore()}>
        <TodoListContainer  />
      </Provider>
    );
    expect(baseElement).toBeTruthy();
  });
});

以上で、React Typescriptでcontainer単体テストをとりあえず実装できます。
Unit/Integrationを利用する場合は比較的つかえるかなとおもいますが、作り込む場合はjestの学習コストがあがるのできっと、人柱が必要になります。

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

React Material の初期開発で<Dialog>いっぱい書きたくない話

概要

dialogやsnackbarをmaterialで記載する時にcontextを利用すると再利用生があがるので、初期開発の速度が上がる可能性がある。

経緯

個人的にも昔のProjectで、[ダイアログ空いてる?、] = useState()てきなことを大量にかいていた戒め。

TL;DR

ReactのClasss Componentを利用してContext Providerを作成。
component側からは、DialogのReactElementを渡す。
ClassComponentを使わない場合については、hooksが利用できないので(hooks in hooksでエラー)死んだ話。

流れ

  1. Reactのプロジェクトを作成
  2. Materialを追加
  3. Class Componentを利用してContextを作成
  4. Dialogの中身を作成(適当に)
  5. Functional Componen側からDialogContextを呼ぶ

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

Nxまたはcreate-react-appを利用してアプリケーションを作成

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

$ create-react-app my-app

nxを利用する場合
terminal
$ npx create-nx-workspace@latest --preset=react

プロジェクトが作成されたら、Materialの追加

2. Materialを追加

$ yarn add @material-ui/core

ここで下準備終了。

3. Class Componentを利用してContextを作成

stateにDialogに必要なopenのflag, elementを利用、
contextValueのcreate, closeのfuncitonを持たせる。
dialogにoptionを追加した場合は、default のoptionを内部で持ちcreateの時にoverrideするといろいろ楽に対応できます。

contexts/dialog-context.tsx
import { Dialog } from '@material-ui/core';
import React, { createContext } from 'react';

// ?はdefaultValueの時に渡せないため、?を利用
export interface DialogContextValue {
  createDialog?: (elm: JSX.Element) => void,
  closeDialog?: () => void,
  open: boolean
}

/* eslint-disable-next-line */
export interface DialogContexttProps {}

interface DialogContextState {
  open: boolean,
  elm: JSX.Element
}

export const DialogContext = createContext<DialogContextValue>({
  open: false
})

export class DialogcontextProvider extends React.Component<DialogContexttProps, DialogContextState> {
  constructor(props) {
    super(props)
    this.state = {
      open: false,
      elm: null
    }
  }

  createDialog = (elm: JSX.Element) => {
    this.setState(state => ({
      ...state,
      open: true,
      elm
    }))
  }

  closeDialog = () => {
    this.setState(state => ({
      ...state,
      open: false,
      elm: null
    }))
  }

  render() {
    return (
      <DialogContext.Provider value={{
        open: this.state.open,
        createDialog: this.createDialog,
        closeDialog: this.closeDialog
      }}>
        { this.props.children }
        <Dialog open={this.state.open}>
          { this.state.elm }
        </Dialog>
      </DialogContext.Provider>
    )
  }
}

export default DialogcontextProvider;

次にcontextを利用するため、Appに追記します。
適宜利用してください。

app.tsx
import React from 'react';

import DialogcontextProvider from './contexts/dialog-contextt';

const App = () => (
  <div>
    <DialogcontextProvider>
      <div>sample</div>
    </DialogcontextProvider>
  </div>
)

export default App

4. Dialogの中身を作成(適当に)

これは、適当でいいですがhooksを内部で利用するとclass Componentじゃないといけないことがわかると思います。
useForm, useDispatch, useSelectorなど結構メジャーどころを使ってcontextProvider ClassComponentじゃない時に、hook in hookのエラーが表示されます。

components/dialog-content.tsx
import React from 'react';

/* eslint-disable-next-line */
export interface DialogContentProps {}

export function DialogContent(props: DialogContentProps) {
  return (
    <div>
      <h1>Welcome to dialog-content!</h1>
    </div>
  );
}

export default DialogContent;

5. Functional Componen側からDialogContextを呼ぶ

最後に呼び出し元のcomponentを作成して、dialog呼べるか確認していきます。

components/button.tsx
import { DialogContent } from './dialog-content';
import React, { useContext } from 'react';
import { DialogContext } from '../contexts/dialog-contextt';

/* eslint-disable-next-line */
export interface ButtonProps {}

export function Button(props: ButtonProps) {
  const { createDialog } = useContext(DialogContext)
  return (
    <div>
      <button onClick={() => createDialog(<DialogContent />)}>click to open</button>
    </div>
  );
}

export default Button;

app.tsxに呼び出し元を追加。

app.tsx
import React from 'react';
import Button from './components/button';

import DialogcontextProvider from './contexts/dialog-contextt';

const App = () => (
  <div>
    <DialogcontextProvider>
      <div>sample</div>
      <Button />
    </DialogcontextProvider>
  </div>
)

export default App

表示されたbuttonをクリックした時にelementが表示されます。

dialog-none.png

opened-dialog.png

地味に利用手段が増えるので、snackbarとかも、入れておくと、

Sample.tsx
function Sample () {
  useEffect(() => {
      createSnackBar({ type: 'error', message: "Api Failっぽいよ" })
  }, [errors])
  return (
  )
}

の様にcomponent側から呼び出すことが可能になり、Componentは一つのため、開発速度は上がります。

Repo

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

React 17 で Recoil を使う

概要

まだ React 17 対応版の Recoil が npm に公開されていないため、npm install recoil で入れるとエラーが出ます。
Nightly Build をインストールすることで React 17 環境でもインストールできます。

インストール

npm install https://github.com/facebookexperimental/Recoil.git#nightly --save

https://recoiljs.org/docs/introduction/installation/#nightly-builds

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

simpackerでRails + React + Typescript環境構築

はじめに

simpackerとは

webpackerを使わず、シンプルなwebpackでRailsを開発するgem。
クックパッド技術部の方が作られたgemです。
https://github.com/hokaccha/simpacker
Simpacker: Rails と webpack をもっとシンプルにインテグレーションしたいのです
Githubと、このクックパッドの開発者ブログを見れば、simpackerが何なのかほとんどわかると思います。

なぜsimpacker?

Rails6以降、webpackerが標準搭載になっていますが、webpackerは独自のDSLになっており、webpack本来の設定などが見えなくなっています。簡単にwebpackを扱えますが、設定のカスタマイズには独自DSLを学ぶコストがついてきます。またwebpackの細かい設定などは調整しにくかったりできないこともあるので、純粋なwebpackを使いたいならsimpackerの導入をおすすめします。
またwebpackerの独自DSLを学ぶなら、webpackそのものを学んだ方が今後の開発に活かせる気がします。私は初めて現場で触れたのがsimpackerの方で、webpackerのことはよくわかってなかったりします。笑

Rails + React + TypeScriptで環境構築

バックエンドはRails、フロントエンドはReact + TypeScriptで構成された開発手順をメモしたいと思います。

開発環境

ruby 3.0.0
rails 6.1.1
yarn 1.22.10
tsc 4.1.3

手順

グローバルを汚さないRails環境構築やMySQLのコンテナ化は別記事に解説してますので、よければ参考にしてください。
Dockerでコンテナ化したMySQLを使用してRails環境構築
Railsの環境構築(グローバル環境を汚さずに)

railsプロジェクト立ち上げ
bundle exec rails new my_app --skip-javascript
bundle exec rails db:create

webpackerを入れたくないので、--skip-javascriptを付けます。

simpackerをGemfileに追加
Gemfile
gem 'simpacker'
gemインストール
bundle install
simpacker初期化コマンド実行
bundle exec rails simpacker:install
React, TypeScript、必要なパッケージをインストール
yarn add -D react react-dom
yarn add -D typescript ts-loader

yarnを使っていますが、npmでも問題ありません。
ts-loaderはtypescriptをjavascriptにトランスパイルするためのパッケージです。

webpack.config.jsを作成

ここにwebpackの設定を書いていきます。
純粋なwebpackと全く同じ書き方です。

webpack.config.js
const path = require("path");
const WebpackAssetsManifest = require("webpack-assets-manifest");

const { NODE_ENV } = process.env;
const isProd = NODE_ENV === "production";

module.exports = {
  mode: isProd ? "production" : "development",
  devtool: "source-map",
  entry: {
    application: path.resolve(__dirname, "app/frontend/js/packs/application.tsx"),
  },
  output: {
    path: path.resolve(__dirname, "public/packs"),
    publicPath: "/packs/",
    filename: isProd ? "[name]-[hash].js" : "[name].js",
  },
  resolve: {
    extensions: [".js", ".ts", ".jsx", "tsx"],
  },
  module: {
    rules: [
      {
        test: /\.(js|ts|jsx|tsx)$/,
        exclude: /node_modules/,
        use: [
          {loader: "ts-loader"}
        ]
      }
    ]
  },
  plugins: [
    new WebpackAssetsManifest({
      publicPath: true,
      output: "manifest.json",
    }),
  ],
};

const path = require("path");
node.jsのpathモジュールを読んでます。

const WebpackAssetsManifest = require("webpack-assets-manifest");
manifest.json
を生成してくれるパッケージです。

const { NODE_ENV } = process.env;
こちらで任意の環境変数を読み込んでいます。

mode:
modeによって出力ファイルの形式が変わります。productionだと圧縮され、develomentだとみやすく整形されて出力されます。

devtool:
ソースマップを指定できます。ソースマップを有効にすると、ブラウザコンソールでエラーを確認するときに、エラー箇所を特定できるため必須だと思います。

entry:
webpackに読み込ませるエントリポイントを指定します。
path.resolve(__dirname, "")と言う表記は環境に依存しない絶対pathを取得できるっぽいです。

output:
ファイルに出力先を指定します。
publicPathは本番環境での解決pathを指定しています。
railsではデフォルトでpublic配下がドキュメントルートなので、/packs/を指定しています。

extensions:
importするファイルの拡張子を省略できます。同じファイル名の異なる拡張子ファイルが存在した場合、配列の先頭のものが読み込まれます。

import File from '../path/to/file';

rules:
トランスパイルするローダーの設定を書きます。
test:で対象ファイルを指定します。
useで使用するローダーをしてします。複数書いた場合は後ろから実行されます。

WebpackAssetsManifest:
manifest.jsonを作成するwebpack用ライブラリです。

webpackの設定はやはり公式を参照すべきだと思います。
https://webpack.js.org/concepts/
こちらも充実しています。
webpack 4 入門 - Qiita

tsconfig.jsonを作成

typescriptの設定を書いていきます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "strict": true,
    "noImplicitAny": false,
  },
  "include": [
    "app/frontend/js/**/*"
  ],
  "exclude": [
    "**/*.(spec|test).ts",
    "**/setup.jest.ts",
  ]
}

target:
出力するjsのバージョンを指定します。

module:
使用するモジュールを指定します。

targetやmoduleについてまだ詳しく理解していないので、今後勉強していきたいと思ってます。

jsx:
reactを使用する場合、reactを指定します。

allowJs:
trueでjsファイルもトランスパイルしてくれます。

moduleResolution:
とりあえずnodeにしておけばいい?

strict:
全ての型チェックを有効にします。

tsconfig.jsonについては、
公式にコンパクトにまとまっています。
https://typescript-jp.gitbook.io/deep-dive/project/compilation-context/tsconfig
日本語だと、こちらの記事でかなり詳細に解説してくださっています。
tsconfig.jsonの全オプションを理解する(随時追加中) - Qiita

Reactコンポーネントを作成

エントリーポイントのファイルを作成していきます。

app/frontend/js/packs/application.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import Index from '../pages/Index';

const appElement = document.getElementById('app');

if (appElement) {
  ReactDOM.render((
    <Index />
  ), appElement);
}

レンダーするコンポーネント

app/frontend/js/pages/Index.tsx
import * as React from 'react';

interface Props {}

const Index: React.FC<Props> = () => {
  return(
    <div>Hello React</div>
  )
};

export default Index;

これをwebpackでビルドしてjsファイルを出力します

yarn webpack

/public/packs/配下にapplication.js, application.js.map, manifest.jsonが作成されていると思います。

これらをRails側で読み込みましょう。

bundle exec rails g controller Top
app/controllers/top.rb
class TopController < ApplicationController
  def index; end
end

app/views/top/index.html.erb
<div id="app"></div>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
  </head>

  <body>
    <%= yield %>
    <%= javascript_pack_tag 'application' %>
  </body>
</html>

simpackerが良い感じに<%= javascript_pack_tag 'application' %>を解釈して読み込んでくれます。

http://localhost:3000/indexにアクセスしてHello Reactが表示されれば成功です。

終わりに

simpackerはシンプルにwebpackを扱えるようにしてくれます。
ただ、今のRailsのフロント周りのgemはwebpackerを前提に作られているものが多いので、そういうgemを利用する際には、かなり苦労することもありますので、少なからずデメリットも存在します。

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

GatsbyブログのSEO対策するべきこと一覧

はじめに

Gatsbyを用いて技術ブログを作ったのは良いものの、数ヶ月の間SEO対策はほったらかしにしていました。最近このブログに色々SEO対策を施したので、その方法を紹介したいと思います。

ちなみにこのブログのstarterはgatsy-starter-hello-worldです。

SEOとは

SEOとはSearch Engine Optimizationの略で日本語にすると「検索エンジン最適化」となります。GoogleやYahooなどの検索エンジンで記事が上位に表示されるようにする対策することをいいます。


Gatsbyは静的なHTMLサイトのSEO対策とは違ったアプローチが必要です。それ故に「SPAはSEOに弱い」などと言われていたのですが、GatsbyのSEO系のプラグインは充実していて、それらを使えば簡単に実装することができます。

Sitemapの生成

Sitemapは検索エンジンのクローラーがサイト構造を読み取るのを助けます。

プラグインにgatsby-plugin-sitemapが用意されているので、これをインストールします。

$ npm i gatsby-plugin-sitemap

gatsby-config.jsにてサイトのURLをなければ追加し、plugins配列に先ほどインストールしたものを追加します。

title=gastby-config.js
siteMetadata: {
  siteUrl: `https://www.example.com`,
},
plugins: [`gatsby-plugin-sitemap`]

きちんとSitemapが作成されたかどうかはhttps://example.com/sitemap.xmlにアクセスすることで確認できます。

またSitemapはSearch ConsoleでGoogleに提出しましょう。

headのmetaタグの設定

React-Helmetを用いて行います。Starterによってはもともと入っているものもあると思いますがなければインストールしてください。React-Helmetはhead内のmetaタグを設定するのに使います。

このブログの場合はHeadというmeta情報を管理するコンポーネントを作成してそこで色々設定しています。

今回はこのコンポーネントで

  1. meta設定
  2. Twitterカードの設定
  3. 言語を日本語に設定

を行ます。

完成したコードはこちらになります。下でさらに詳しい解説をします。

title=head.js
const Head = ({ title, description, lang, meta }) => {
  const data = useStaticQuery(graphql`
    query {
      site {
        siteMetadata {
          title
          siteUrl
          description
        }
      }
    }
  `)
  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={`${title} | ${data.site.siteMetadata.title}`}
      meta={[
        {
          name: `description`,
          content: `${data.site.siteMetadata.description}`,
        },
        {
          name: `twitter:card`,
          content: `summary`,
        },
        {
          name: `twitter:creator`,
          content: data.site.siteMetadata.author,
        },
        {
          property: `og:image`,
          content: `${data.site.siteMetadata.siteUrl}/images/tube.png`,
        },
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: `${data.site.siteMetadata.description}`,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          name: `thumbnail`,
          content: `${data.site.siteMetadata.siteUrl}/images/tube.png`,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: `${data.site.siteMetadata.description}`,
        },
        {
          property: `og:type`,
          content: `website`,
        },
      ]}
    />
  )
}


サイトの言語の設定

サイトの言語を日本語に設定します。HelmetにhtmlAttribute=langを設定して、defaultPropsをjaにします。
```js:title=head.js
//..省略
<Helmet
htmlAttributes={{
lang,
}}

//..省略

Head.defaultProps = {
  lang: `ja`,
  meta: [],
  description: ``,

}
```

Metaの設定

autherやsiteTitleなどの情報はgatsby-config.jsに入れておいて、そこから取り出せるようにしておきます。

title=gatsby-config.js
module.exports = {
  siteMetadata: {
    title: "k-log",
    author: "Kebeb",
    description: `Kebebの技術ブログ。主にMERN stackの学習の記録`,
    siteUrl: `https://jujekebab.com/`,
  },

headコンポーネントのgraphQLからアクセスして設定します。

title=gatsby-config.js
        {
          property: `og:description`,
          content: `${data.site.siteMetadata.description}`,
        },

Twitterカードの設定

TwitterなどのSNSで共有した時に、いい感じのカードが表示されるようにします。
og:image,twitter:title,twitter:creator,twitter:cardさえ設定しておけば最低限の見た目にはなります。

  • 最低限の見た目の例

作成したカードはTwitter Card Validatorを用いて下見することができます。

Urlの正規化

Canonical属性を指定します。これもプラグインで簡単に実装できます。
- gatsby-plugin-canonical-urls

npm i gatsby-plugin-canonical-urlsして、gatsby-config.jsに追加するだけです。オプションでサイトのURLを追加しましょう。

title=gatsby-config.js
plugins: [
    {
      resolve: `gatsby-plugin-canonical-urls`,
      options: {
        siteUrl: `https://jujekebab.com`,
        stripQueryString: true,
      },
    },
]

robot.txt の設置

クローラーに読み取ってほしいページと読み取ってほしくないページを伝えます。

こちらもgatsby-plugin-robots-txtというプラグインが用意されているので、これを使います。

これもインストールしてgatsby-config.jsに追加するだけです。

Google Search コンソールでの登録

Google Analyticsを登録しているなら、そこからGoogle search Consoleへリンクすると簡単だと思います。

LightHouseでも高得点

LighHouseでもSEOは100点でした。あとはコンテンツの質だけだ!

Screen Shot 2021-02-19 at 19.16.27.png

まとめ

こういう目に見えない細かい作業をすると、はてなブログなどの無料ブログサイトの有り難みがわかります。もしGatsbyを使っていなかったらと思うとどれだけの作業量になるのか…。

構造化データの追加だけはやっていなかったので次回はそれについて紹介したいと思います。

参考

WPからGatsbyへ移行時に気を付けたいSEO対策一覧と導入方法

Gatsbyの検索エンジン最適化(SEO)の全まとめ

【2021年版】Google サーチコンソール の使い方。ウェブサイト改善のための活用術!

Gatsby: SEO対策(最低限やっておくこと)

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

Reactの重要キーワード

Reactの重要キーワード

state

・ユーザーの動きに合わせて変わる値。
・stateの値を更新するときはsetState()を使う。

コンポーネント

・Reactでは、見た目を機能ごとにコンポーネント化して、コンポーネントを組み合わせることでWebサイトの見た目を作る。
・
コンポーネントとは要はUIの部品。

props

コンポーネントに渡すデータ

jsx

・見た目を作る部分。htmlのように書ける。
・一つの要素の中に書く。
・jsx内でコメントは以下のように書く

 {/* コメント */}

・{}内はjs

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

パイソンのイラスト生成WebアプリをReactで作った

パイソンについて

うすうす感づいているかもしれませんが、この記事にはPython言語の話は出てきません。それでもこうしてこの記事に出会えたのも何かのご縁、ちょっとだけでも覗いていっていただけませんか。

普段Python言語を使っているソフトウェアエンジニアの方でも本物のパイソンを見たり触ったりしたことのある人は少ないのではないでしょうか。今日はそんなパイソンの魅力だけでもおぼえて帰っていただければと思います。

パイソンは日本語でニシキヘビと呼ばれていますが日本の野生では生息していません。美しい模様と、いかつい顔立ちが特徴のヘビです。

毒を持たず、鳴き声やにおいもなく、アレルギー源となる体毛もなく、餌は週に1回、寿命も10〜20年と長く飼いやすいため、犬や猫を飼えない事情がある人でも飼育できるペットとして近年人気が出ています。

なかでもボールパイソンは性格もおとなしく、大きくなりすぎないためおすすめです。

モルフとは

模様や色で分類される品種をモルフと呼びます。ボールパイソンは非常に多彩なモルフが特徴で、希少で人気のあるモルフは高額で取引されます。飼育下でのヘビの繁殖はそれほど難しくなく、好みのモルフを掛け合わせてあらたなモルフを作出する愛好家も多いです。

そのためここ数年で膨大な種類のモルフが生み出されてきました。これらのモルフの特徴をおぼえるのが大変なので作ったのが、今回紹介するウェブアプリBall Python Virtual Morph Makerです。

Ball Python Virtual Morph Maker

https://smallpinkmouse.github.io/virtualmorph/
https://github.com/smallpinkmouse/virtualmorph
screenshot.png

色や模様の特徴をGUIで設定することができます。望むパターンができたら、Renderingボタンを押すとトグロを巻いたイラストが生成されます。

フレームワークはReactを使用、グラフィックの描画はp5.js、スライダーはrc-slider、カラーピッカーはreact-colorを使っています。

設定したパラメータをjson形式でローカルに保存して、後から再現できるようにしています。

モルフの特徴

ノーマルモルフのボールパイソンは逆三角形の斑紋(Blotch)とその中の2つの点が特徴で、宇宙人の顔に見えるためAlien Headと呼ばれています。
normal_pattern.png

モハベモルフの場合、斑紋の黄色味が強くなり中の点の数もひとつになります。Keyholeとも呼ばれます。
mojave_pattern.png

スパイダーモルフの場合は、斑紋が大きくなり隙間が蜘蛛の糸のように細くなります。中の点は消失します。
spider_pattern.png

生成したイラスト

レンダリングしたイラストはこのようになります。

ノーマルモルフの頭部は黒く、目はクリっとしています。
normal.png

野生下ではニュースになるくらい希少なアルビノも、爬虫類ブリーダーの間では遺伝を管理されているため、入手しやすいポピュラーなモルフとなっています。黄色の色素は残るため体色は完全に白にはならず、また目は赤くなります。
albino.png

パイドとかパイボールと呼ばれる、印刷ミスのように模様が白く抜けるモルフも人気があります。
pied.png

スパイダーは前述のように蜘蛛の巣のような模様が特徴です。頭部や虹彩も少し色が抜けて、猫目になる個体が多いようです。
spider.png

横の斑紋が完全に消失して、背中の線だけが高速道路のセンターラインのように残るフリーウェイという品種です。
freeway.png

最後に

ボールパイソンがいかに多彩で美しい模様を持つかが伝わったでしょうか。
苦手な人もいるかと思うので、実物の写真はここまで出しませんでしたが、もし興味がわいたなら世界最大のボールパイソン情報サイトWorld of Ball Pythonsを見てみてください。
http://www.worldofballpythons.com/

最後に我が家で飼っているナムパイ君をごらんください。パステルクラウンというパステルとクラウン両方の特徴を引き継いだモルフになります。
numpy.png

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