20200320のReactに関する記事は12件です。

Next.js(React)でCSSとSASS(SCSS)、Bootstrapを使う

Next.jsでCSSとSASS(SCSS)、Bootstrapを使う

next.jsでreactのSPAを作り始めたのですが、css, sass, bootstrapの設定周りですこしつまづいたのでメモ。

Next.jsのプロジェクト作成

プロジェクト作成

yarn craete next-app

これでNext.jsのプロジェクトが作成されます。 yarn dev で起動できます。

CSS, SASSの設定

css, sass(scss)ファイルをimportできるようにします。

まずはパッケージインストールします。

yarn add @zeit/next-css

yarn add @zeit/next-sass node-sass

next.config.jsの作成

プロジェクトのルートに next.config.js ファイルを作成します。これはnext.jsの設定ファイルです。

以下はcssとsassどちらもimportできるような設定です。

next.config.js
const withSass = require('@zeit/next-sass');
const withCSS = require('@zeit/next-css');
module.exports = withCSS(withSass());

Bootstrapの設定

Bootstrapを入れます。

yarn add bootstrap

yarn add react-bootstrap

Bootstrapを適用します。
アプリ全体に適用したいので、グローバルに設定したいです。

まず、/pages/_app.js を作成します。
next.jsでは、全体設定のためのインターフェースとして _app.js で設定できるようになっています。

_app.js でbootstrapをimportすることでアプリ全体に適用していくのです。

/pages/_app.js
import 'bootstrap/dist/css/bootstrap.min.css';

function MyApp ({ Component, pageProps }) {
  return <Component {...pageProps} />
};

export default MyApp;

ページを作ってみる

では正しく設定できているか実際にページを作って確認してみましょう。

/pages/login.js
import {Container, Row, Col, Form, Button} from 'react-bootstrap';

const Login = () => {
  return (
    <Container className='login'>
      <Row>
        <Col className='text-center'>
          <h2>ログイン</h2>
        </Col>
      </Row>
      <Row>
        <Col>
          <Form className='col-6 offset-3'>
            <Form.Group>
              <Form.Control type='text' name='id' />
            </Form.Group>
            <Form.Group>
              <Form.Control type='password' name='password' />
            </Form.Group>
            <Form.Group>
              <Form.Control type='submit' name='submit' value='ログイン' className='btn btn-primary' />
            </Form.Group>
          </Form>
        </Col>
      </Row>
    </Container>
  );
};

export default Login;

確認してみる

起動します

yarn dev

localhost:3000/login にアクセス。

スクリーンショット 2020-03-20 23.55.26.png

わあい

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

Next.jsでCSSとSASS(SCSS)、Bootstrapを使う

Next.jsでCSSとSASS(SCSS)、Bootstrapを使う

next.jsでreactのSPAを作り始めたのですが、css, sass, bootstrapの設定周りですこしつまづいたのでメモ。

Next.jsのプロジェクト作成

プロジェクト作成

yarn craete next-app

これでNext.jsのプロジェクトが作成されます。 yarn dev で起動できます。

CSS, SASSの設定

css, sass(scss)ファイルをimportできるようにします。

まずはパッケージインストールします。

yarn add @zeit/next-css

yarn add @zeit/next-sass node-sass

next.config.jsの作成

プロジェクトのルートに next.config.js ファイルを作成します。これはnext.jsの設定ファイルです。

以下はcssとsassどちらもimportできるような設定です。

next.config.js
const withSass = require('@zeit/next-sass');
const withCSS = require('@zeit/next-css');
module.exports = withCSS(withSass());

Bootstrapの設定

Bootstrapを入れます。

yarn add bootstrap

yarn add react-bootstrap

Bootstrapを適用します。
アプリ全体に適用したいので、グローバルに設定したいです。

まず、/pages/_app.js を作成します。
next.jsでは、全体設定のためのインターフェースとして _app.js で設定できるようになっています。

_app.js でbootstrapをimportすることでアプリ全体に適用していくのです。

/pages/_app.js
import 'bootstrap/dist/css/bootstrap.min.css';

function MyApp ({ Component, pageProps }) {
  return <Component {...pageProps} />
};

export default MyApp;

ページを作ってみる

では正しく設定できているか実際にページを作って確認してみましょう。

/pages/login.js
import {Container, Row, Col, Form, Button} from 'react-bootstrap';

const Login = () => {
  return (
    <Container className='login'>
      <Row>
        <Col className='text-center'>
          <h2>ログイン</h2>
        </Col>
      </Row>
      <Row>
        <Col>
          <Form className='col-6 offset-3'>
            <Form.Group>
              <Form.Control type='text' name='id' />
            </Form.Group>
            <Form.Group>
              <Form.Control type='password' name='password' />
            </Form.Group>
            <Form.Group>
              <Form.Control type='submit' name='submit' value='ログイン' className='btn btn-primary' />
            </Form.Group>
          </Form>
        </Col>
      </Row>
    </Container>
  );
};

export default Login;

確認してみる

起動します

yarn dev

localhost:3000/login にアクセス。

スクリーンショット 2020-03-20 23.55.26.png

わあい

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

【REACT】stateを学びましょう!

概要

stateを理解し、使って見ましょう!

state?

REACTでstateは、Component内部で変えることができる値
REACTには、二種類のstateがあります。

  • class型Componentが持っているstate
  • 関数型ComponentでuseStateという関数で使うstate

class型Component

簡単な数字カウンター

count.js
import React, { Component } from "react";

class Count extends Component {
  constructor(props) {
    super(props);
    // stateの初期値を設定
    this.state = {
      number: 0
    };
  }

  render() {
    // stateを呼ぶとき使う
    const { number } = this.state;
    return (
      <div>
        <h1>{number}</h1>
        <button
          onClick={() => {
            // setStateを使って、stateに新しいvalueを設定
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Count;

結果

関数型Component

簡単な数字カウンター

Say.js
import React, { useState } from "react";

const say = () => {
  const [message, setMessage] = useState("");
  const onClickEnter = () => setMessage("Hello");
  const onClickLeave = () => setMessage("Good Bye");

  return (
    <div>
      <button onClick={onClickEnter}>Enter</button>
      <button onClick={onClickLeave}>Leave</button>
      <h1>{message}</h1>
    </div>
  );
};

export default say;

結果

問題

【Red】、【Blue】ボタンを追加して、ボタンをクリックしたら、
<h1>{message}</h1>の messageの色が変わるようにしましょう!

hint
styleを使いましょう!

正解

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

[React]コンポーネントの再帰処理でディレクトリ構造を表示する

コンポーネントの再帰処理をやってみた。

サンプルはこちら

ディレクトリ構造を表すようなイメージで作成してみたけど、サイドバーとかドロップダウンメニューとか、いろいろと使えそう。

import React, { FC, Children } from 'react';

// ディレクトリ構造としてのサンプルデータ
const fileNames = [
  { name: 'file A' },
  { name: 'file B' },
  {
    name: 'file C',
    items: [
      { name: 'file C-1' },
      { name: 'file C-2' }
    ],
  },
  { name: 'file D' },
  {
    name: 'file E',
    items: [
      { name: 'file E-1' },
      { name: 'file E-2' },
      { name: 'file E-3' },
      {
        name: 'file E-4',
        items: [
          { name: 'file E-4-1' },
          { name: 'file E-4-2' }
        ],
      },
    ],
  },
];

// items で files を受ける。 depth は階層を表す。
const RecursionComponent: FC<{ items: any; depth: number }> = ({
  items,
  depth,
}) => {
  return (
    <>
      {Children.toArray(
        items.map((item: any) => (
          <>
            <div style={{ marginLeft: depth * 20 }}>
              - {item.name}
            </div>
            {item.items && (
              <RecursionComponent items={item.items} depth={depth + 1} />
            )}
          </>
        )),
      )}
    </>
  );
};

const Recursion: FC = () => (<RecursionComponent items={fileNames} depth={0} />);

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

【React】 Driver.jsでチュートリアル風ハイライト

概要

初めて起動したアプリなどでよく見るチュートリアルで利用されている、ハイライトをReactを使って再現したいと思います!
(巷では、「コーチマーク」と呼ばれているようでしたが、言葉知らないとなかなか検索ヒットしないよね。)

その後、ちょっと自分好みにUIを変えてみました。

iOS版は、こちらの記事で詳細されてました。
https://qiita.com/tkt989/items/2981497580beae09875d

Driver.js

今回は、JavaScriptライブラリで、Driver.jsというものを使ってみます。

Light-weight, no-dependency, vanilla JavaScript engine to drive user's focus across the page

と、説明されています!公式サイトはこちら

こんなのが作れます!
Image from Gyazo

出典 : https://kamranahmed.info/driver.js/

Reactで開発

では、早速作っていきます!

React環境構築 Driver.jsの導入

npmにもありましたので、こちらを使っていきます!

React×Material-UI環境にDriver.jsをインストールして、簡単ではありますが、
ボタンがクリックされたら、ハイライトが起動するように準備していきます。

import React from "react";
import Button from "@material-ui/core/Button";
import Driver from "driver.js";

class App extends React.Component {
  handleClick = props => {
    // ハイライト発火の処理記述
  };
  render() {
    return (
      <React.Fragment>
        <div>
          <h1>Hello, Driver.js</h1>
          <Button
            onClick={this.handleClick}
            variant="outlined"
            color="secondary"
          >
            click
          </Button>
        </div>
      </React.Fragment>
    );
  }
}

export default App;

Driver.js エレメントを指定する

ここまでで、handleClickメソッドに処理を記入すればハイライトできるようにできました。

公式サイトのサンプルコードを見ると、めちゃめちゃ簡単に実装できそうですね。

const driver = new Driver();
driver.highlight('#create-post');

hightlightメソッドにエレメントをしてするだけで簡単にいけそう!
(本当に!?)

Buttonコンポーネントにidを指定しました。

styleファイルをimportする

ダメだった...

公式のソースに設定したidを設定してみましたが、なぜか、ボタンが消えた...
Image from Gyazo

全然ダメじゃん...と思いながら、
一度、公式サイトに戻ってよく見ると...

Or grab the code from dist directory and include it directly.

<link rel="stylesheet" href="/dist/driver.min.css">
<script src="/dist/driver.min.js"></script>

スタイルファイルが必要なのか...

ちゃんと公式は読みましょうね!
改めて、cssファイルをimportしてトライするとうまくいきました!
Image from Gyazo

コーチマークの表示

ハイライトは実装できたので、コーチマークを表示させます。

hightLightメソッドはオブジェクト型で値を渡すことができます。

const driver = new Driver();
driver.highlight({
  element: "#block",
  // ↓コーチマークの記述
  popover: {
    title: "クリック",
    description: "ここをクリックする事で〇〇できます。",
    position: "right"
  }
})

Screenshot from Gyazo

さらに、連結したコーチマークの表示なども可能です。
詳細は、公式サイトでご確認ください!

自分好みにカスタマイズ

透明度やハイライト範囲の余白などの変更が可能です。
ちょっと、四角で囲っているのが、クールではないので、ぼかしを入れてみました。
CodeSandboxのリンク置いてますので、ソース確認してみてください!
Image from Gyazo
Edit driver

後付け感があって、ちょっと使い物になりませんね。
ちょっと工夫の必要がありそうです。。

さいごに

今回は、Driver.jsを使って、
ハイライト、コーチマークのチュートリアル風インタラクションをReactで再現してみました。
ライブラリによって、得意不得意あると思いますし、
Reactとの相性がいいものがあればいいなぁ。

知っている方いましたら是非教えていただきたい。。

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

[React]uniq key がないときの `Each child in a list should have a unique "key" prop.` の回避方法

サンプル作ってたり、データにユニークな key がないときに出る Each child in a list should have a unique "key" prop. のエラーが面倒だなと思ってたんですが、 React.Children.toArray で回避できる。

<>
  {React.Children.toArray(items.map(item => <div>{item.title}</div>))}
</>

データ構造が非公開の children を平坦な配列として返し、それぞれの要素に key を割り当てます。

https://ja.reactjs.org/docs/react-api.html#reactchildrentoarray

便利!

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

Noodl react library ガイド(和訳)

この記事は、Noodl公式のSlackコミュニティー上でMathias L氏が2019年9月に公開した「Noodl react library guide」の和訳になります。

※注:和訳は意訳+注釈を追加しています。


Noodl Reactライブラリガイド(和訳)

このガイドではReact LibraryをNoodlワークスペースに追加する方法を紹介します

ワークスペースフォルダが適切に設定されていることを確認してください

まずはNoodl CLI toolがインストールされていることを確認してください。
ツールはnpmを使ってインストールできます。

$ npm install -g noodl-lab-cli

次に、空のワークスペースフォルダを作成します。
このフォルダにすべてのカスタムコンポーネントとモジュールを含めます。

$ mkdir my-workspace-folder
$ cd my-workspace-folder

すべてをゼロから開始する必要はありません。
Noodlスターターラボのテンプレートが使⽤できます。

$ curl -O https://s3.amazonaws.com/updates.noodlcloud.com/lab
s/noodl-starter-lab.zip
$ unzip noodl-starter-lab.zip

ワークスペースフォルダーではじめ、このワークスペースをNoodlクラウドワークスペースに接続します。
initコマンドを実⾏できます。下記のXXXをあなたが取得したアクセスキーに置き換え、提供されているワークスペース名に置換えるてください。

$ noodl-lab init --accessKey "XXX" --name "your-workspace-nam
e"

アクセスキーはワークスペースの管理ページにあります(labとワークスペースどちらも使用でき、同じものを参照します)。

※訳注: このアクセスキーはNoodlワークスペースの管理者にしか公開されていません。残念ながら、ベータテスタ権限ではアクセスキーを確認することができません。

image400.png

⻭⾞をクリックして管理ページに⼊ります。ラボ名がユーザ名の下に表⽰されます

image401.png

アクセスキーは管理ページの下部にあります

ワークスペースのコンテンツを変更するたびにpushする必要があります。
また、pushすることでコンテンツフォルダが適切に設定されていることをテストできます。

$ noodl-lab push

上記のコマンドが正常に実⾏されると、ワークスペースフォルダーが正しく設定されます。Noodlモジュールの共同作業ができるので、このフォルダをバージョン管理下に置いておくとよいでしょう。

訳注:Noodl管理者ではないので、このガイドで紹介されている「noodl-lab」コマンドを使いNoodlクラウドワークスペースへ追加することはできません。
ですが、下記のReactモジュールを作成しNoodlプロジェクトに追加することはできます。

新しいReactライブラリモジュールを作成する

ここでワークスペースフォルダタイプに新しいライブラリモジュールを追加します。

$ cd library
$ curl -O https://s3.amazonaws.com/updates.noodlcloud.com/lab
s/react-module-template.zip
$ unzip react-module-template.zip

これでワークスペースライブラリフォルダに2つのフォルダーと1つのアイコンファイルが追加されます。

feed
library/
newmodule-project
newmodule
newmodule-icon.png

作成しているライブラリモジュールに合わせて、2つのフォルダとアイコン名を変更します。
例えば「mymodule-project」と「mymodule」のように。

$ mv newmodule mymodule
$ mv newmodule-project mymodule-project
$ mv newmodule-icon.png mymodule-icon.png

まずは「mymodule」フォルダを⾒てみましょう。
ここにはモジュールのソースコードを含めます。
たとえばすべてのReactコードとNoodlを結合するコード、webpackを使⽤するビルドスクリプトなど。
まずpackage.jsonファイルのモジュール名を変更します。

{
"name"︓ "com-your-domain-module-name"
...
}

名前をモジュールの代表的な名前に変更しビルドを進めると。
すべての依存関係あるモジュールがインストールされます。
訳注:「mymodule」フォルダでnpm installを実行すると、「node_modules」フォルダが作成され、package.jsonに書かれている依存関係のあるモジュールが自動でダウンロードされます。

$ npm install

テンプレートモジュールにはサンプルコードが含まれているため、そのままでビルドができます。

$ npm run build

このステップではReactモジュールをビルドし、別フォルダのmymodule-projectにコピーします。このフォルダーはNoodlで開いて編集、テストできるNoodlプロジェクトが含まれています。
訳注:npm run bulidによってwebpackがproductionモードで実行されます。これで自作モジュールに必要なファイルをindex.jsにまとめます。
さらに、mymodule-projectフォルダにNoodlに必要な「noodl_modules」フォルダを作成します

Noodlを起動し、[Project]タブに移動して、ページ下部の[Import existing project]を選択します。

image402.png

プロジェクトページの下部にある[Import existing project]ボタンをクリックします

重要 [Save local]チェックボックスがオンになっていることを確認してください。インポートするプロジェクトがローカルのコンピュータに保存され、
インポート元の場所で編集されます。例ではワークスペースフォルダ内のプロジェクトを編集します。

image403.png

「ローカルに保存」がチェックされていることを確認してください︕

プロジェクトに名前を付けて(任意の名前でかまいません)、Pick project folderボタンを押してください。

image404.png

プロジェクトに名前を付けて、「Pick project folder」をクリックします

ワークスペースディレクトリ(上記のテンプレートを解凍し作成されたディレクトリ)「mymodule-project」フォルダを探し、[開く]をクリックします。

開いたプロジェクトには、サードパーティのReactコンポーネントのようないくつかのカスタムReactコンポーネントが表⽰されます。このコンポーネントを使用したりプロパティを調べることができます。ではモジュールのコードを詳しく⾒てみましょう。

「mymodule」フォルダに戻ると、srcフォルダの下位にindex.jsが含まれているのがわかります

mymodule/
  assets/
  node_modules/
  src/
    index.js
  package.json
  package-lock.json
  webpack.config.json

これはReactコンポーネントをNoodlにインポートする、すべての結合用コードを含むファイルです。
もしモジュールに新しいコンポーネントを追加するなら、npmを介してパッケージをインストールするか、コードをモジュールフォルダーにコピーし結合用コードをindex.jsファイルに追加してください。

MyCustomReactComponentとサードパーティパーティコンポーネントFieldRangeの2つの例をご覧になり、これがどのように⾏われているか確認することをお勧めします。
訳注:サンプルプログラムもこの記事の下部に掲載しています。

モジュールのソースコードを変更した場合は再構築する必要があります。
開発モードでビルドステップを起動することができます。

$ npm run dev

これで変更が監視され、新しいモジュールが⾃動的にビルドされてNoodlプロジェクトにコピーされます。
変更を加えたら、Ctrl/Cmd-Rを使ってNoodlビューアを更新するだけです。

モジュールをライブラリに追加する

自作モジュールに満足したら、自分のワークスペースライブラリにモジュールを追加できるだけでなく、ほかのNoodlユーザープロジェクトにも含めることができます。

そのためには、ライブラリフォルダー内のindex.jsonファイルをモジュールに合わせて編集してください。

[
   {
      "label":"My React Module",
      "desc":"I made this myself!",
      "thumb":"mymodule-icon.png",
      "project":"mymodule-project"
 }
]
  • label:ライブラリモジュールのラベル
  • desc:簡単な説明
  • thumb:ライブラリパネルにアイコンとして表⽰される画像
  • project:プロジェクトフォルダの名前。プロジェクトの名前でなければなりません。モジュールフォルダではなくプロジェクトフォルダです。

これが完了したら、ワークスペースフォルダのルートに戻り、
新しいコンテンツをNoodlクラウドワークスペースにプッシュします。

$ noodl-lab push

プッシュ後、Noodlを再起動する必要があります。再起動するとエディター内のLIBRARYタブに新しいモジュールが表示されます
訳注:ここでもnoodl-labコマンドを使えないため、手動で追加していきます。
「noodl_module」フォルダごとNoodlプロジェクトのリソースフォルダ(左のバーからProjectSetting>Open Project folderボタンをクリックして表示されるフォルダ。Windowsでは「C:\Users\user\Documents\Noodl」にある各プロジェクトフォルダでした。)にコピーすることで、Noodlに自作モジュールを追加します。

image405.png

左のバーからLIBRARYタブを選択すると、新しいモジュールが表示されます。

これで、ユーザーは[ADD]ボタンをクリックしてプロジェクトにモジュールを追加できます。

 (追加)サンプルコード

最後に上記例で説明されているカスタムReactコンポーネントのサンプルコードがISCライセンス(GNU GPLと両立する自由ライセンス)で提供されているので和訳化して掲載します

index.js
import FieldRange from '@atlaskit/range'; // https://atlaskit.atlassian.com/packages/core/range

//サードパーティReactコンポーネントの例
//Third party react component example

const AtlasKitRange = {
    //ノード名、Noodlで呼ばれるもの。必須
    name: 'Atlas Range', //name of the node, this is what it'll be called in Noodl. Mandatory
    //カテゴリも必須
    category: 'Atlas', //A category is also mandatory.

    //ここでReactコンポーネントを返す必要があります
    //Here we need to return the React Component.
    getReactComponent() {
        return FieldRange;
    },

    //Reactコンポーネントに転送されるすべての入力リスト
    //A list of all inputs that will be forwarded to the react component
    inputProps: {
        isDisabled: {type: 'boolean', default: false},
        max: {type: 'number', default: 100},
        min: {type: 'number', default: 0},
        value: {type: 'number'}
    },
    //出力プロパティはNoodlノードの出力で生成される以外、inputPropsと非常に似ています
    //これらは通常Reactのコールバックで、Noodlは値をキャッチして出力として送信します
    //these props are very similar to the inputProps, except that they will generate outputs on the Noodl node
    //these are typically callbacks in React, and Noodl will catch the value and and send it as an output
    outputProps: {
        onChange: {
            type: 'number',
            displayName: 'New Value',
            // Reactコンポーネントに送信されるカスタムコールバックを定義できます。Noodlはここで出力で送信する値が返ってくることを期待しています
            // この例は非常に単純なので、カスタムの「getValue」関数を定義する必要はありません。
            // Noodlが自動的に判断しgetValu関数を補完します 
            // You can define a custom callback that's sent to the React component. Noodl expect you to return a value here
            // that Noodl will send on the output. In this case its so simple that we don't need to define a custom 'getValue' function.
            // Noodl will automatically figure it out 
            // getValue: sliderValue => sliderValue
        }
    }
}

//カスタムReactコンポーネントの例
//Custom react component example

function MyCustomReactComponent(props) {
    const style = {
        color: props.textColor,
        backgroundColor: props.backgroundColor,
        borderRadius: '10px',
        padding: '20px',
        marginBottom: props.marginBottom
    };

    return <div style={style} onClick={props.onClick} >{props.children}</div>
}

const MyCustomReactComponentNode = {
    name: 'Custom React Component',
    category: 'Tutorial',
    getReactComponent() {
        return MyCustomReactComponent;
    },
    inputProps: {
        backgroundColor: {type: 'color', default: 'white'},
        marginBottom: {type: {name: 'number', units: ['px'], defaultUnit: 'px'}, default: 10}
    },
    outputProps: {
        onClick: {type: 'signal', displayName: 'Click'}
    }
}


Noodl.defineModule({
    reactNodes: [
        AtlasKitRange,
        MyCustomReactComponentNode
    ],
    nodes:[
    ],
    setup() {
        //起動時に一度呼び出されます
        //this is called once on startup
    }
});

図Noodl上に取込まれた自作ノード
image441.png

まとめ

Noodl向けにReactコンポーネントを自作し、Noodlノードとして登録したかったので、一連の流れを調べてみました。
自作や外部のReactコンポーネントをNoodノード化できると、Noodlの可能性がまた広がりますね。

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

第7回 2020年版 React+Firebaseで画像のアップロード(その2)

1. 概要

前回の記事でFirebaseの環境を構築が完了しましたが、今回は実際に画像をアップロードするプログラムを開発します。

2. 前提条件

作業日時

  • 2020/3/20

環境

  • MacBook Pro
  • macOS Catalina

ソフトウェアのバージョン

分類 ソフトウェア バージョン
フレームワーク React 16.13.0
フレームワーク React-dom 16.13.0
静的型付け TypeScript 3.7.5
Firebase CLI firebase-tools 7.14.0
ライブラリ Material UI v4.9.4
@material-ui/core 4.9.4
@material-ui/icons 4.9.1
ライブラリ react-dropzone 10.2.1

3. 追加のライブラリのインストール

Material UIに加えて、画像をドラッグアンドで追加可能とするためreact-dropzoneをインストールする。

$ yarn add @material-ui/core @material-ui/icons
$ yarn add react-dropzone

4. Firebaseのコンフィグファイルの作成

Firebaseのアカウント情報を保持するconfigファイルを作成します。 コンフィグファイルのAPI keyなどの入力項目はFirebaseのWebコンソールから確認できます。
「プロジェクトの設定」 > 「全般」 > 「マイアプリ」 > 「ウェブアプリ」 > 「マイアプリ」 > 「Firebase SDK snippet」 > 「構成」に記載されています。

以下では直接値を記載していますが、環境変数で指定してdatabaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URLといった形で値を指定する方が望ましいです。

/src/firebase_config.ts
import * as firebase from 'firebase/app';

//必要なモジュールごとにimport
// import 'firebase/auth';
// import 'firebase/firestore'; 
// import 'firebase/database';
import 'firebase/storage';
// import 'firebase/function'; 
// import 'firebase/analytics';

// インスタンスの初期化
const firebaseConfig = {
  apiKey: "hoeghogehogehoeghogehogehoeghogehoge",
  authDomain: "hoge",
  databaseURL: "https://hoge.firebaseio.com",
  projectId: "hoge",
  storageBucket: "hoge.appspot.com",
  messagingSenderId: "9999999999",
  appId: "1:1000000000000:web:hogehogehogehoge"
};

export const firebaseApp = firebase.initializeApp(config);
export default firebaseApp;

Storageのアクセス権限修正

今回はサンプルのため、未認証でもファイルの書き込みが可能なように、Storageのアクセス権限を付与します。

storage.rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
//      allow read, write: if request.auth!=null;
      allow read, write;
    }
  }
}

`firebase deploy'で設定を反映します。

5. プログラムの作成

App.tsx

最初にApp.tsxFileUploadのコンポーネントを表示するように修正します。

src/App.tsx
import React from "react";
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import CssBaseline from "@material-ui/core/CssBaseline";
import green from '@material-ui/core/colors/green';

import FileUpload from "./FileUpload";

// 独自のテーマを作成する
const theme = createMuiTheme({
  palette: {
    //type: 'dark', // ダークテーマ
    primary: green,
  },
  typography: {
    fontFamily: [
      'Noto Sans',
      'sans-serif',
    ].join(','),
    fontSize: 12,
    h1: {
      fontSize: "1.75rem"
    },
    h2: {
      fontSize: "1.5rem"
    },
    h3: {
      fontSize: "1.25rem"
    },
    h4: {
      fontSize: "1.125rem"
    },
    h5: {
      fontSize: "1rem"
    },
    h6: {
      fontSize: "1rem"
    },
  }
});

function App() {
  return (
    <MuiThemeProvider theme={theme}>
        <CssBaseline />
        <FileUpload />
    </MuiThemeProvider >
  );
}

export default App;

FilreUpload.tsx

次に、FileUpload.tsx のコンポーネントを作成します。

useDropzone でDropzoneの設定と、ファイルがドロップされた時に呼びされる関数 onDrop を指定します。 onDropの中ではドロップされたファイルをfilesに保存してます。

このfilesに保存された画像ファイルはサムネイルとして、下部に表示しています。Masonryっぽく全てのタイルが埋まるようにしています。ちゃんと実装するなら以下のようなライブラリを利用してください。

Firebase storageへのアップロードは、アップロードボタンが押下された時に呼び出されるonUpload関数の中で行っています。

const storageRef = firebaseApp.storage().ref().child('images/' + file_name);
でアップロード先を指定し、var task = storageRef.put(file); でアップロードを行っています。 taskはアップロード処理の途中経過・完了の把握や、処理の停止をするためのものです。

Promise.all で各ファイルを並列にアップロードを行い、全てアップロードが完了したら、ローディングの中止とアラートの表示を行っています。

src/FileUpload.tsx
import React, { useState, useEffect, useCallback } from "react";

import * as firebase from 'firebase/app';
import { firebaseApp } from './firebase_config';

import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';

import {
  Typography,
  Grid,
  GridList,
  GridListTile,
  GridListTileBar,
  Button,
  IconButton,
  Paper,
  CircularProgress
} from '@material-ui/core/';

import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import InfoIcon from '@material-ui/icons/Info';

import { useDropzone } from 'react-dropzone'

// スタイルを適用する
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      display: 'flex',
    },
    paper: {
      padding: theme.spacing(2),
      textAlign: 'center',
      '& > *': {
        margin: theme.spacing(3),
      },
    },
    dropzone: {
      width: "100%",
      height: 200,
      boxSizing: "border-box",
      borderWidth: 2,
      borderColor: "#666666",
      borderStyle: "dashed",
      borderRadius: 5,
      verticalAlign: "top",
      marginRight: "2%",
    },
    thumbsContainer: {
      marginTop: 16,
    },
    gridList: {
      width: "100%",
      height: 450,
      // Promote the list into his own layer on Chrome. This cost memory but helps keeping high FPS.
      transform: 'translateZ(0)',
    },
    titleBar: {
      background:
        'linear-gradient(to bottom, rgba(0,0,0,0.7) 0%, ' +
        'rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)',
    },
    icon: {
      color: 'white',
    },
    upButton: {
      color: "secondary",
      margin: theme.spacing(3),
    },
    circular: {
      textAlign: 'center',
    }
  }),
);

// propsは無し
type Props = {};

// Dropzoneの設定
const acceptFile = 'image/*';
const maxFileSize = 1048576;

// previewを追加
type MyFile = File & {
  preview: string;
};

export default function FileUpload(props: Props) {
  console.log("FileUpload page start.");

  // State
  const [files, setFiles] = useState<MyFile[]>([]);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const classes = useStyles(props);

  /*
  ドロップした時の処理
  */
  const onDrop = useCallback((acceptedFiles: File[]) => {
    console.log('onDrop');

    // previewの追加
    setFiles(acceptedFiles.map(
      file => Object.assign(file, {
        preview: URL.createObjectURL(file)
      })));

  }, [])

  // Dropzone
  const { getRootProps, getInputProps, isDragActive }
    = useDropzone({ onDrop, accept: acceptFile, minSize: 0, maxSize: maxFileSize })

  const onUpload = async () => {
    console.log('onUpload start');

    // ローディングをOn。progressを初期化
    setUploading(true);
    setProgress(0);

    function uploadImageAsPromise(file) {
      console.log('uploadImageAsPromise start');

      // アップロード先のファイルパスの作成
      const file_name = file.name;
      const storageRef = firebaseApp.storage().ref().child('images/' + file_name);

      return new Promise(function (resolve, reject) {
        //Upload file
        var task = storageRef.put(file);

        //Update progress bar
        task.on(firebase.storage.TaskEvent.STATE_CHANGED,
          function progress(snapshot) {
            var percent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
            console.log(percent + "% done");
          },
          function error(err) { // 失敗時
            console.log("upload error");
            reject(err);
          },
          function complete() { // 成功時
            console.log('upload complete.');
            task.then(function (snapshot: firebase.storage.UploadTaskSnapshot) {
              resolve(snapshot.ref.getDownloadURL());
            })
          }
        );

      }).then(function (downloadURL) {
        console.log("Finished uploading file: " + file_name);

        // progressを更新する
        setProgress(oldProgress => (oldProgress + 1));
        return downloadURL;
      }).catch(function () {
        console.log("Error:uploadImageAsPromise");
      });
    }

    // 複数のファイルアップロードをPromise.allで並列に実行する
    const result = await Promise.all(files.map((file) => { return uploadImageAsPromise(file); }));

    console.log("Upload result");
    console.log(result);

    // ローディングを終了し、リストを空に
    setUploading(false);
    setProgress(0);
    setFiles([]);

    alert("送信されました");

  }

  // アップロード中はCircularを表示する
  if (uploading === true) {
    const percent = Math.round((progress / files.length) * 100)
    console.log("Loadingの表示。Progreass:" + progress + " Percent:" + percent);

    return (
      <Grid container className={classes.root} spacing={3} justify="center">
        <Grid item xs={6}>
          <Paper variant="outlined" elevation={3} className={classes.paper}>
            <CircularProgress className={classes.circular} variant="determinate" value={percent} />
          </Paper>
        </Grid>
      </Grid>
    )

  } else {
    // タイルを敷き詰められるように、一部画像のサイズは大きくする
    const tile_cols = 3;
    let tile_featured = [];
    switch (files.length % tile_cols) {
      case 0:
        tile_featured = [];
        break;
      case 1:
        tile_featured = [0, files.length - 1];
        break;
      case 2:
        tile_featured = [0];
        break;
    }

    // サムネイルの作成
    const thumbs = files.map((file, index) => (

      <GridListTile key={file.preview} cols={tile_featured.indexOf(index) >= 0 ? 2 : 1} rows={1}>
        <img src={file.preview} alt={file.name} />
        <GridListTileBar
          title={file.name}
          subtitle={file.size}
          actionIcon={
            <IconButton aria-label={`star ${file.name}`} className={classes.icon}>
              <InfoIcon />
            </IconButton>
          }
          actionPosition="left"
          className={classes.titleBar}
        />
      </GridListTile>
    ));

    const diabled_button = (files.length === 0);

    return (
      <Grid container className={classes.root} spacing={3} justify="center">
        <Grid item xs={6}>
          <Paper variant="outlined" elevation={3} className={classes.paper}>

            <Typography variant="h4">Upload image files to GCS</Typography>
            <div>
              <Paper className={classes.dropzone} {...getRootProps()}>
                <input {...getInputProps()} />
                {
                  isDragActive ?
                    <p>Drop the files here ...</p> :
                    <p>Drag 'n' drop some files here, or click to select files</p>
                }
              </Paper>
              <Button onClick={onUpload} variant="outlined" color="primary" disabled={diabled_button} className={classes.upButton} startIcon={<CloudUploadIcon />} >Upload</Button>
              <aside className={classes.thumbsContainer}>
                <GridList cellHeight={200} className={classes.gridList} cols={tile_cols}>
                  {thumbs}
                </GridList>
              </aside>
            </div>
          </Paper>
        </Grid>
      </Grid>
    );
  }

}

6. 動作確認

yarn startでサーバーを起動し、ブラウザで表示します。

ReactDropzoneサンプル.gif

FirebaseのWebコンソールでファイルがアップロードされていることを確認します。

2020-03-20_Firebase_storage.png

7. 最後に

次回はアップロードした画像をCloud Functionsでリサイズする方法について説明します。

8. 関連記事

Reactに関する記事です。

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

React+Typescript+SWRでアプリをつくる

SWR という面白そうなライブラリに遭遇し、気になったので簡単にアプリを作ってみようと思います。

構成はReact+Typescript+SWRです。

今回はReact+Typescriptの環境構築の仕方などは書きません。
create-react-appを使うなり、webpackを使うなり自分の好きな方法で構築して下さい。

APIはGitHubさんが公開しているAPIがあるのでそちらを使っていきたいと思います。

API(GitHub)

ライブラリ公式(英語のみ)

環境

MacBookPro v10.14.6
node v10.15.3
yarn v1.15.2
React v16.13.0
Typescript v3.8.3
SWR v0.1.18

インストール

皆さんそれぞれReact Appを作成したとして、今回のメインであるSWRをインストールします。

# 今回、僕はyarnを使っていますがnpmでも問題ないです
# SWRはtypescriptに対応しているので@typesをインストールする必要はないです。

yarn add swr

使用するAPI

今回、GitHub APIの中でもユーザーネームを入れたらそのユーザーのpublicリポジトリを返すAPIを使いたいと思います。

# usernameのリポジトリを返す
https://api.github.com/users/:username/repos

** GitHub APIは1日にリクエストを送れる回数が決まっているので、気をつけて下さい。

実装

ここからアプリを実装していきたいと思います。
まず、今回必要なモノをimportするのと雛形を作成していきます。

App.tsx
import React from 'react';
// インストールしたSWR
import useSWR from 'swr';

const App: React.FC = () => {}

export default App;

ここまできたら、GitHub APIを使ってデータを取得する為のコードを追加していきます。

App.tsx
import React from 'react';
import useSWR from 'swr';

const App: React.FC = () => {
 // 追加
  const fetcher = (
    url: string
  ) => fetch(url)
         .then(res => res.json)
         .catch(error => error)

  // facebookのリポジトリを取得します
  const { data, error } = useSWR(
    "https://api.github.com/users/facebook/repos", 
    fetcher
  )
}

export default App

これだけでAPIへのリクエスト、レスポンスの取得ができます。
めちゃくちゃシンプルじゃないですか???

ここまで来たら完成したようなもの、後はViewに表示させたいと思います。

App.tsx
const App: React.FC = () => {
  const fetcher = (
    url: string
  ) => fetch(url)
         .then(res => res.json)
         .catch(error => error)

  const { data, error } = useSWR(
    "https://api.github.com/users/facebook/repos", 
    fetcher
  )

  // 追加
  // errorとloadingの部分は公式のホームページに載っているものです
  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return (
    <div>
      // 今回は、anyにしています。
      {data.map((d: any) => (
        <a href={`https://github.com/${d.full_name}`}>
           // d.full_nameで facebook/:repo_name みたいな値が取れます。
           {d.full_name}
        </a>
      ))}
    </div>
  )
}

export default App

ここまでやるとこうなります。

スクリーンショット 2020-03-19 19.33.49.png

僕の場合styleをつけているので少し違うと思いますが、facebookのリポジトリを取得することができました!

完成形

完成形がこちらです。

App.tsx
import React from 'react';
import useSWR from 'swr';

const App: React.FC = () => {
  const fetcher = (
    url: string
  ) => fetch(url)
         .then(res => res.json)
         .catch(error => error)

  const { data, error } = useSWR(
    "https://api.github.com/users/facebook/repos", 
    fetcher
  )

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return (
    <div>
      {data.map((d: any) => (
        <a href={`https://github.com/${d.full_name}`}>
           {d.full_name}
        </a>
      ))}
    </div>
  )
}

export default App

今回、触ってみて思ったのは本当にシンプルで読みやすい。
個人的にかなりオススメなライブラリなので、皆さんもお試しあれ。

使用したモノ

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

React Native × React Navigation v5 で Deep linking

はじめに

またしても React Navigation v5 の記事です。
今回は Deep linking を実装していきます。

下準備

iOS 用の設定をします。

URL Typesの設定をおこないます。

Screen Shot 2020-03-19 at 19.39.02.png

次に、 AppDelegate.mを修正します。

Screen Shot 2020-03-19 at 19.48.08.png

コードはこちらです。

#import "AppDelegate.h" // 元からある
#import <React/RCTLinkingManager.h>

...

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
 restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
 return [RCTLinkingManager application:application
                  continueUserActivity:userActivity
                    restorationHandler:restorationHandler];
}

これだけで、 Safari などで<URL Schemas>://xxxxのように入力すれば、アプリを開くことができます。
Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-19 at 22.44.05.png

これでアプリへのアクセスはできるようになりました。
各ページへの遷移は React Navigation と組み合わせて使うと便利です。
以下ではその方法を実装していきます。

React Navigation v5 で Deep linking

公式ドキュメントを参考に、useLinkingを使用します。

const App: React.FunctionComponent = () => {
  const ref = useRef();
  const { getInitialState } = useLinking(ref, {
    prefixes: ['srn5://'],
    config: {
      Details: 'details',
    },
  });
  const [isReady, setIsReady] = useState(false);
  const [initialState, setInitialState] = useState();

  useEffect(() => {
    Promise.race([
      getInitialState(),
      new Promise((resolve) => setTimeout(resolve, 150)),
    ])
      .catch((e) => {
        console.error(e);
      })
      .then((state) => {
        if (state !== undefined) {
          setInitialState(state);
        }

        setIsReady(true);
      });
  }, [getInitialState]);

  if (!isReady) {
    return null;
  }

  return (
    <AuthProvider>
      <NavigationContainer initialState={initialState} ref={ref}>
        <HomeNavigator />
      </NavigationContainer>
    </AuthProvider>
  );
}

refを定義して、NavigationContaineruseLinkingに渡しています。
useLinkingconfigでパスと画面を紐づけています。

HomeNavigatorは下記の形で定義しています。

import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

const Stack = createStackNavigator();
const HomeNavigator = () => (
  <Stack.Navigator>
    <Stack.Screen name="Home" component={Home} />
    <Stack.Screen name="Details" component={Details} />
  </Stack.Navigator>
)

通常のアクセスだとHomeが表示されますが、srn5://detailsにアクセスするとDetailsが直接開きます。

おわりに

今回は、 Deep linking の実装について紹介しました。

実際に利用する場合、ユニバーサルリンク(https://~~~)という形で利用するのが現実的かと思いますが、まずはこの方法で簡易的なアプリへのリンクを設定することができます。

参考

https://reactnative.dev/docs/linking#handling-deep-links
https://reactnavigation.org/docs/deep-linking/

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

create-react-appで作成したアプリのデプロイにおける環境変数の扱いについて

環境

  • create-react-app 3.4.0
  • gh-pages 2.2.0

create-react-appでの環境変数について

ReactやNodeなどで環境変数を使いたい場合には、dotenvをインストールする必要がありますが、create-react-appを使用した場合だと、デフォルトでdotenvが組み込まれています。

このため、追加のインストールをせずとも環境変数が設定できます。
ただし、create-react-appの場合はREACT_APP_から始まる変数名をつけなくてはなりません。
.envファイルの例は以下のようになります。

.env
REACT_APP_API_KEY="YOURKEY"

環境変数にアクセスするには次のようになります。

App.js
render () {
  return (
    <div>
      {process.env.REACT_APP_API_KEY}
    </div>
  )
}

デプロイする際の注意点

環境変数の読み込みにはいくつかモードがあり、
ローカル実行時には、.env.development
テスト実行時には、.env.test
ビルド時には、.env.production
というふうになっています。

このため.envしか用意せず、ビルドを行い、デプロイをすると、環境変数が読み込まれない場合があります。

解決法

この解決法として、.envファイルを複数モード用に分ける方法があります。
私は以下のように分けました。

  • 開発用
.env.development
REACT_APP_API_KEY="YOURKEY"
  • 本番用
.env.production
REACT_APP_API_KEY="YOURKEY"

このように分けることで、npm run buildgh-pages -d buildを行った際に、生成したjsファイルに環境変数が埋め込まれるようになります。

参考文献

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

CSS Modules の問題点をミックスインで解決する

はじめまして、よこけんです。(2回目)
今日は、CSS Modules の問題点を解決する方法について検討したのでそれを共有してみます。

CSS Modules の問題点

CSS Modules ではクラスセレクタによるスタイル適用が基本です。
そしてクラス名は一つのコンポーネントに複数指定することができます。
しかし、複数指定した場合の適用順序は保証されません。クラス名の指定順序ではなく、クラスセレクタが読み込まれた順序に依存します。

例えば下記の場合、背景色は赤色ではなく青色になります。

a.styl
.a
    background-color: red
b.styl
.b
    background-color: blue
Hoge.tsx
import * as aStyles from "./a.styl";
import * as bStyles from "./b.styl";
const Hoge = () => <div className={`${bStyles.b} ${aStyles.a}`}>Hoge</div>;

サンプルコードでは Hoge.tsx がインポート順序を b.styl -> a.styl に変えれば意図した結果を得ることができますが、例えば Hoge.tsx から参照される別のコンポーネントで b.styl をインポートしている場合にはやはり .b が優先されて青色になってしまいます。
この問題は非常に厄介な上に、直接的な解決方法というものはありません。

ミックスインによる解決

本記事では、この問題に対する比較的扱いやすい解決方法として、Stylus や Sass のミックスイン機能を利用した解決方法を紹介します。
ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。全てのプロパティが集約されることにより、単純にミックスインの指定順序に従ってプロパティが適用されます。ミックスイン本体の読み込み順序は関係ありません。

次のサンプルコードでは、mixHoge()mixB() の後に mixA() を呼んでいるため、背景色は必ず赤色になります。

a.styl
mixA()
    background-color: red

.a
    mixA()
b.styl
mixB()
    background-color: blue

.b
    mixB()
Hoge.styl
@import "a.styl"
@import "b.styl"

mixHoge()
    mixB()
    mixA()

.hoge
    mixHoge()
Hoge.tsx
import * as styles from "./Hoge.styl";
const Hoge = () => <div className={styles.hoge}>Hoge</div>;

シンプルなルール

前述のコードをルール化すると次のようになります。

  • 一つのコンポーネントに複数のクラスを指定してはいけない
  • クラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する
  • クラスセレクタでは常に、対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う
  • クラス継承 (@extends) は一律禁止とし、代わりにミックスインを使用する

クラス継承 (@extends) は一律禁止ということに気を付けてください。
単一継承であれば大丈夫のように思うかもしれませんが、クラス継承を使ってしまうと、そこから先をミックスインで派生させても全てのプロパティが一箇所に集約されなくなってしまい、読み込み順序に再び依存するようになってしまいます。

この方式の欠点は、トランスパイルされた CSS ファイルのサイズが肥大化するリスクです。
前述の通り、ミックスインはクラス継承と違い、適用位置にプロパティを全てコピーします。だから読み込み順序に一切依存しなくなるわけですが、これはトランスパイル後の CSS ファイルのサイズに影響します。
スタイルの継承を多用するようなプロジェクトではリスクが顕在化するかもしれません。
リスクが顕在化してきた場合には、プロパティ数が多く継承も多く行われる特定のスタイルに対してのみクラス継承 (@extends) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。

追加ルールでリスクを軽減

先ほどはクラス継承を使用すると問題に繋がるとしていましたが、厳密には、派生を許可しないクラスからであればクラス継承を使用しても問題には繋がりません。
合法的にクラス継承を使用できるケースが発生すると、ファイルサイズを抑える効果が期待できます。

次のサンプルコードは、.hoge クラスの派生を禁止することで安全を確保できます。

a.styl
mixA()
    background-color: red

.a
    mixA()
b.styl
mixB()
    background-color: blue

.b
    mixB()
Hoge.styl
@import "a.styl"
@import "b.styl"

.hoge
    @extends .b
    mixA()
Hoge.tsx
import * as styles from "./Hoge.styl";
const Hoge = () => <div className={styles.hoge}>Hoge</div>;

これをルール化すると次のようになります。

  • 一つのコンポーネントに複数のクラスを指定してはいけない
  • 派生を許可するクラスセレクタを用意する場合、対になる単一のミックスインを必ず用意する
  • 派生を許可するクラスセレクタでは常に対になる単一のミックスインの適用のみを行い、スタイル記述はミックスイン内で行う
  • ミックスイン内でのクラス継承 (@extends) は一律禁止とし、代わりにミックスインを使用する
  • 派生を許可しないクラスセレクタではスタイルを直接記述して良い
  • 派生を許可しないクラスセレクタではクラスを一つだけ継承 (@extends) して良い (2つ以上のクラスを継承したい場合はミックスインを併用する)

この方式の欠点は二つあります。

一つはルールが少し複雑になるために混乱を招いたりルール違反が発生しやすくなることです。
ただし、派生を許可するクラスセレクタがあまり多くない (整理されていて見通しが良い) プロジェクトなら、追加ルールを適用しても混乱やルール違反は最低限に抑えられると思います。

もう一つは、リスクの低減はできても完全に回避することはできないということです。
リスクが顕在化してきたら最初の解決方法と同様、特定のスタイルに対してクラス継承 (@extends) を許可し、それらのクラスセレクタだけは読み込み順序を慎重に管理します。
しかし、元々少し複雑なルールにこの例外措置が加わることになりますので、混乱やルール違反をより招きやすくなる恐れがあります。

結論

  • リスクが顕在化する可能性が低そうであればシンプルなルールを採用する
  • 追加ルールを採用する場合、混乱やルール違反を招かないよう工夫する

なお、そもそも複数クラスの継承をしようとしなければ問題は起きません。ただし、そのためのルールは結局必要になります。 (そして恐らく、そのルールによって新たなリスクも発生します。)

根本的には、クラスセレクタの読み込み順序ではなくクラス指定順序で結果が決まってくれれば良いんですが、CSS の仕様のようなので。

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