ふんばりフロントエンジニアのブログ

新米フロントエンジニアの備忘録です。ふんばり温泉タオル欲しい...

ReactのListのkeyを正常に指定しないととんでもないことになる

今回もGyazoのテストも兼ねて、ReactのListのkeyについての投稿です。

あんまやる人いないと思うのですが、keyにを正常に指定、つまり一意なkeyを指定しないと全体的にレンダリングされてしまうので注意が必要です。

ja.reactjs.org

上記見ていただくとわかるのですが、Reactの差分検出処理は双方のリストの一致/不一致を走査し、不一致があればその差分を更新するというものです。

で、例でもあるのですが最後に追加された場合は、問題なく「差分は1つ」の更新になります。
ただ、先頭に追加された場合は最初から不一致となるので、「差分は3つ」となります。つまり全て差分がある、と認識されてしまうんですね。

そこで登場するのが、keyという概念。

上記では、単純なDOMの比較をしていましたが、keyを比較することにより、より正確かつ素早く差分を検出することができるようになります。

では、例を見てみましょう。

一意なkeyを指定した場合と、ランダムなkeyを指定した場合です。

ランダムなkeyを指定した場合(key = Math.random)

gyazo.com

const CountUpList = () => {
    const [list, setList] = useState([{id: 1, name: 'test1'}]);
    const countRef = useRef(1);


    const handleCountUp = () => {
        countRef.current = countRef.current + 1;
        const addedObj = {
            id: countRef.current,
            name: `test${countRef.current}`
        }

        setList([...list, addedObj])
    }

    return (
        <>
            <div>
                <div>カウント: {countRef.current}</div>
                <button onClick={handleCountUp}>カウントアップ</button>
            </div>
            <ul>
                {list.map(el => <li key={Math.random()}>{el.name}</li> )}
            </ul>
        </>
    )
}
正常なkeyを指定した場合(key = el.id)

gyazo.com

const CountUpList = () => {
    const [list, setList] = useState([{id: 1, name: 'test1'}]);
    const countRef = useRef(1);


    const handleCountUp = () => {
        countRef.current = countRef.current + 1;
        const addedObj = {
            id: countRef.current,
            name: `test${countRef.current}`
        }

        setList([...list, addedObj])
    }

    return (
        <>
            <div>
                <div>カウント: {countRef.current}</div>
                <button onClick={handleCountUp}>カウントアップ</button>
            </div>
            <ul>
                {list.map(el => <li key={el.id}>{el.name}</li> )}
            </ul>
        </>
    )
}


ご覧の通り、ただkeyを指定すればよいというわけではなく、このList内で一意なkeyを設定することが必要になります。
Math.randomだと重複ありますからね。

APIの返り値を表現する際にもListは結構使われると思いますが、APIの設計もちゃんとこの辺りを考えられると良いですね。
惰性で振っているIDかもしれませんが、SPAで構築する際にはわりと重要なポイントになってくると思います。

ということで、やっぱGyazoいいですな...

cracoでwebpackの設定をオーバーライドする

hubarifront.hatenablog.com

前回の記事の続きです。
前回はcraco自体のプラグインを使って、グローバルにmixinを定義する方法などをご紹介しましたが、今回はwebpackの設定のオーバーライドについて。

cracoは「creat-react-app」の設定をいじれるライブラリで、create-react-appではwebpackが使われています。

さて、どんなconfigが使われているのかみてみましょう!...といきたいところなのですがコード量多いので、気になる方は「node_modules/react-scripts/config/webpack.config.js」を見てみてください。

では、実際にオーバーライドしてみたcraco.config.jsを見てみます。

const sassResourcesLoader = require('craco-sass-resources-loader');
const TerserPlugin = require('terser-webpack-plugin');
const path = require("path");


module.exports = {
    mode: process.env.REACT_APP_ENVIROMENT,
    output: {
        path: __dirname
    }
    webpack: {
        alias: {
            "@": path.resolve(__dirname, "src"),
        },
        configure: {
            optimization: {
                minimize: true,
                minimizer: [
                    new TerserPlugin({
                        terserOptions: {
                            ecma: undefined,
                            parse: {},
                            compress: {},
                            mangle: true,
                            module: false,
                        },
                    }),
                ],
            },
        },
    },
    // craco plugin setting
    plugins: [
        {
            plugin: sassResourcesLoader,
            options: {
                resources: './src/css/style.scss',
            },
        },
    ]
};

見てもらうとわかりますが、「webpack」というkeyがありますよね。
ここにもろもろ入れるとwebpackの設定をオーバーライドすることができます。

今回は、aliasの設定とTerserの設定を変えてみました。

まんまwebpack.config.jsをwebpackのキーの中に入れるだけだと思っていいです。

aliasを設定すると、いちいち相対パスで遡る必要がなくなるので便利ですよね。

ただ、今回ちょっと詰まったのはTerserのバージョンです。

create-react-appの内部では、Terserのバージョンが4系だったのに対し、最初私は5系の最新を入れてしまったためビルドエラーとなりました。
create-react-appで使用されているライブラリをオーバーライドする際には、しっかりバージョンを合わせる必要がありそうです。

とにかく、割と簡単にオーバーライドできるので、webpackをいちいち設定するよりはcracoを使ってプロジェクトに合わせて設定をオーバーライドしていくのは、スマートな気がしました。

FLIP手法でクリックした要素と先頭を入れ替えるUIを作る

FLIP手法というものを使って、表題の要件を実現してみました。

参考にさせていただいたのは、下記です。
www.dkrk-blog.net

手順を書くと、
1. 並べ替え前の位置を取得
2. 要素をinsertBeforeで入れ直し、並べ替え後の位置に配置
3. 並べ替え後の位置を取得
4. 1と3の差分をアニメーションさせる

という感じです。

初めてこの手法を試してみましたが、二つの並べ替え程度であればかなりライトに書けたので、ライブラリとか必要なさそうですね。

デザイナーさんにデザイン発注するときに注意したいこと

最近デザインを発注することが増えた気がしてます。

で、いざ送られてきた時に「あれ?」的なことがあったので、その事例も踏まえて先に言っておくべきこと・注意したいことをまとめていきます。

デザインの単位は「px」指定で!

webデザイナーの方であれば、問題ないと思います。
ただ紙媒体のデザイナーの方だと単位を「mm」で扱うことが多いので、「px」で作ってもらうよう言っておきましょう。

デザイン幅の決め方

PCの場合、サイトのコンテンツ内容によってデザイン幅は変わりますが、一般的には私は下記で発注することが多いです。

SP: 750px
PC: 1200px (コンテンツ幅: 最大1024px)

SPの場合ターゲットとしているデバイスは「iPhone 7/8」です。

PCのコンテンツ幅はiPadのブレイクポイントですね。

こだわるサイトの場合はタブレット用のデザインももらいますが、基本的にSPを拡大するかPCを縮小する形で実装することが多いです。

で、気をつけたいのが画像の解像度について。
hubarifront.hatenablog.com

過去の記事でも書いてますが、基本的にRetina対応すると思いますので画像の解像度はデザイン幅の二倍に耐えうる解像度でなくてはなりません。
なので、上記ではPC:1200pxと書いてますが2400pxで作成してもらってもいいと思います。

ただ、PCは実装上pxとかrem指定するので、実装する際に倍のサイズだとちょっとだるいです。

アウトライン化は必要なところだけ

紙媒体のデザイナーさんは一般的にポスターなどの入稿の際、全てデータをアウトライン化します。
そのため、テキストで表示するようなところまでアウトライン化されてしまうので、画像として使って欲しいところ・文字として使って欲しいところでアウトライン化を分けていただくようお願いしておくといいです。


以上です。都度、更新していきます。

【Shopify】「カートに追加する」ボタンの文言変更についての注意点

shopify最近触ってます。

表題の件ですが、現在無料テーマの「Supply」をカスタマイズしていて、開発はthemeKitでやってます。

で、商品詳細ページでカートに追加するボタンがデフォルトでは下記のように記載されています。

<button type="submit" name="add" id="addToCart-{{ section.id }}" class="{{ btn_class }} btn--add-to-cart">
    <span id="addToCartText-{{ section.id }}">{{ 'products.product.add_to_cart' | t }}</span>
</button>

「'products.product.add_to_cart'」の部分を変えればいけそうですよね。

ということで、この定義は「locales/ja.json」に定義されているので、下記のように変えました。

 "product": {
      "sold_out": "売り切れ",
      "will_not_ship_until": "{{ date }}まで配送できません",
      "quantity": "個数",
      "add_to_cart": "カートに入れる",  //カートに追加する から変更した
      "unavailable": "お取り扱いできません",
      "will_be_in_stock_after": "{{ date }}入荷予定",
      "unit_price_label": "単価",
      "only_left": {
        "one": "残り{{ count }}個!",
        "other": "残り{{ count }}個!"
      },
      "full_details": "詳細"
    }
  },


さて完了、と思っていると他の部分は変更した通りに文言が変わっていたのですがなぜかボタンだけ変わっていない...

なんかリロードした時に書き換わるような挙動だったので、theme.js.liquidを見ると下記の記述が。

// We have a valid product variant, so enable the submit button
this.settings.selectors.$addToCart.removeClass('disabled').prop('disabled', false);
this.settings.selectors.$addToCartText.html({{ 'products.product.add_to_cart' | t | json }});
$(this.settings.selectors.$shopifyPaymentButton, this.$container).show();

書き換えてますね。

でも読み込み方は同じなので、なんで更新されないんだろうと思ってました。

とりあえず、適当に変更して更新したところ、文言が変更されていました。
まああれです、ビルドしなおさないといけないってことですね。
「theme watch」コマンドはあくまでshopifyのサーバーに都度ビルド→アップするコマンドなので、そりゃそうかという感じですが...

以上です。

【craco】create-react-appでsassの変数やらmixinやらをグローバルに定義する方法

React Nativeに続いて、久々にバニラなReactを触りました。

以前はwebpackでチコチコ環境構築していたのですが、最近そういうのだるくなってきたのでcreate-react-appでいいかなと思いまして。

「create-react-app」はreactを即動かせるテンプレートプロジェクトを作成することができるコマンドです。

詳しくは下記の記事など。
qiita.com

で、すぐ作成できるのはいいのですがデフォルトだと色々よしなにやってくれすぎていて、カスタマイズがちょっとだるいんですよね。(ejectとかやんないといけないんです)

さらに言えば、「webpackどうせいじるならwebpackで最初からやったらええやん...」という。

ejectしたらどうなるか的なのは下記の記事など。
qiita.com

で、表題の件に辿り着いたわけですね。

とりあえず、create-react-appをデフォルトにwebpackなどの設定をオーバーライドできるパッケージがあるようで、その名も「craco」と言います。

僕が見つけたのは表題の件を調査していた時で、DEV Communityにて下記の投稿が発端です。

dev.to

パッケージ情報は下記です。

www.npmjs.com

週間で20万DLなのでかなり使われてますよね。

で、とりあえずインストールしてみて、DEV Communityの記載通り「craco-sass-resources-loader」を使用して下記のように設定しました。

const sassResourcesLoader = require('craco-sass-resources-loader');

module.exports = {
    mode: "development",
    output: {
        path: __dirname
    },
    plugins: [
        {
            plugin: sassResourcesLoader,
            options: {
                resources: './src/css/style.scss',
            },
        },
    ]
};

style.scssには下記のような感じで各種変数定義や関数をインポートしてます。

@import "reset";
@import "variables";
@import "functions";
@import "common";

これで、style.scssがグローバルに定義され、どこからでもアクセスすることができるようになりました。
もちろんモジュール化したscss内でも使用することができます。

.artists {
  @include flex(column, center, center);

  & > a {
    > p {
      color: $black;
    }
  }
}

てきな。

個人的にはある程度までcreate-react-appでやってもらって、そのあとはcracoでカスタマイズしていく方針が自分に合っていると感じました。

もちろん、「モジュール化せずに全てstyle.scssでインポートするぜ!」とかの場合は上記の工程必要ないのですが、せっかくReact使うんだからcss modulesとか使いたいですよね。余計な記述なくなりますし。

あとcssファイルを.scssとか、.module.scssとかにリネームするだけでよしなに処理してくれるようになった点はめっちゃ感動しました。

以上です。