20210325のReactに関する記事は6件です。

フロントエンド開発者のための刺激的なプロジェクト10選 1選目考察【前編】

下記の記事をみてフロントエンドエンジニアになりたい気持ちが強くなったので、1選ずつコード見ていき素人ながらに分析しました。かいつまんで、こんな書き方してるんだーみたいな発見を書いていくだけなので、体系的な説明にはなっていないと思うので悪しからず。お願いします。

Object.keys

オブジェクトがもっているキー名を配列として返す。
このプロジェクトではサーバーにおいているSVGへのパスをオブジェクトにまとめてあり、それをObject.keysを用いて、img要素を作るために利用している。

const CONSTANTS = {
  assetPath: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/184729",
}

const ASSETS = {
  head: `${CONSTANTS.assetPath}/head.svg`,
  waiting: `${CONSTANTS.assetPath}/hand.svg`,
  stalking: `${CONSTANTS.assetPath}/hand-waiting.svg`,
  grabbing: `${CONSTANTS.assetPath}/hand.svg`,
  grabbed: `${CONSTANTS.assetPath}/hand-with-cursor.svg`,
  shaka: `${CONSTANTS.assetPath}/hand-surfs-up.svg`
}

// Preload images
Object.keys(ASSETS).forEach(key => {
  const img = new Image();
  img.src = ASSETS[key];
});

けど、Object.keysの部分を消しても正常に動作しているっぽいので、何のためにこれをやっているのかは理解できていない。

カスタムフック Ref addEventListener

戻り値としてrefとstate(真偽値を示すhoveredという変数)の2つの値配列を返すuseHoverというカスタムフックを作成している。
GrabZoneという関数コンポーネントでuseHoverを呼び出し、refをouterRef変数,innerRef変数に、stateをouterHovered変数,innerHovered変数にそれぞれ分割代入している。
outerRef変数,innerRef変数がref属性に指定されたReact要素に、useHover関数内で定義されたaddEventListenerが設定され、カーソルがそこにホバーするたびにuseHover関数内のenter関数が呼び出され、カーソルが範囲外に行くことでleave関数が実行される。
それによってsetHoverdが実行されhoverd変数の真偽値が切り替わり、if文の条件分岐により、UIに変化をもたらしている。

const useHover = () => {
  const ref = useRef();
  const [hovered, setHovered] = useState(false);

  const enter = () => setHovered(true);
  const leave = () => setHovered(false);

  useEffect(
    () => {
      ref.current.addEventListener("mouseenter", enter);
      ref.current.addEventListener("mouseleave", leave);
      return () => {
        ref.current.removeEventListener("mouseenter", enter);
        ref.current.removeEventListener("mouseleave", leave);
      };
    },
    [ref]
  );

  return [ref, hovered];
};

const GrabZone = ({ cursorGrabbed, gameOver, onCursorGrabbed }) => {
  const [outerRef, outerHovered] = useHover();
  const [innerRef, innerHovered] = useHover();
  const [isExtended, setExtendedArm] = useState(false);

  let state = "waiting";
  if (outerHovered) {
    state = "stalking";
  }
  if (innerHovered) {
    state = "grabbing";
  }
  if (cursorGrabbed) {
    state = "grabbed";
  }
  if (gameOver) {
    state = "shaka"
  }

  // If state is grabbing for a long time, they're being clever!
  useEffect(() => {
      let timer;
      if (state === "grabbing") {
        timer = setTimeout(() => {
          // Not so clever now, are they?
          setExtendedArm(true);
          timer = null;
        }, 2000);
      }
      return () => {
        setExtendedArm(false);
        if (timer) {
          clearTimeout(timer);
        }
      };
    },
    [state]
  );

  return (
    <div className="grab-zone" ref={outerRef}>
      <div className="grab-zone__debug">
        <strong>Debug info:</strong>
        <p>Current state: {state}</p>
        <p>Extended arm: {isExtended ? "Yes" : "No"}</p>
      </div>
      <div className="grab-zone__danger" ref={innerRef}>
        <Grabber
          state={state}
          gameOver={gameOver}
          extended={isExtended}
          onCursorGrabbed={onCursorGrabbed}
        />
      </div>
    </div>
  );
};

イベントハンドラー

このプロジェクトで扱われているイベントは下記の4つ。

mousemove

マウスなどのポインティングデバイスで、カーソルのホットスポットが要素内にある間に動いた時に発行されるイベントです。

ここではwindow内でカーソルが動くというイベントに対して、マウスの位置をプログラムが把握するために使用されている。

mouseenter

ポインティングデバイス (通常はマウス) のホットスポットが最初にイベントが発生した要素の中に移動したときに Element に発生します。

mouseleave

mouseleave イベントは、ポインティングデバイス (ふつうはマウス) のカーソルが Element 外に移動したときに発行されます。

mouseentermouseleaveはある要素内へのカーソルの出入りを検知し、stateの真偽値を更新するために使用されていた。

const useHover = () => {
  const ref = useRef();
  const [hovered, setHovered] = useState(false);

  const enter = () => setHovered(true);
  const leave = () => setHovered(false);

  useEffect(
    () => {
      ref.current.addEventListener("mouseenter", enter);
      ref.current.addEventListener("mouseleave", leave);
      return () => {
        ref.current.removeEventListener("mouseenter", enter);
        ref.current.removeEventListener("mouseleave", leave);
      };
    },
    [ref]
  );

  return [ref, hovered];
};

resize

windowサイズの変更を検知し、要素の寸法と位置を返すgetBoundingClientRectを呼び出している。

つづく

コードみたら何をしているのかは推測がつくのですが、このコードを作り上げる発想がすごいと思いました。
引き続きコード読み解いていきます。

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

styled-componentsで`Received "true" for a non-boolean attribute`というエラーが出たときの対処法

下記のようにstyled-componentsでactivatedというプロパティがtrueのときにはfont-weight: bold;になるようなコンポーネントを作りたいとします。
※記法はjsxを採用

WrapperComponent.jsx
import React from "react";
import styled, { css } from "styled-components";

const TextComponent = (props) => {
    return (
        <span {...props} className={props.className}>
            テキストテキストテキストテキスト
        </span>
    );
};

export const WrapperComponent  = styled(TextComponent)`
    color: #999;
    font-size: 14px;

    ${(props) =>
        props.activated &&
        css`
            color: #000;
            font-weight: bold;
        `}
`;
利用元のPage.jsx
import React from "react";
import { WrapperComponent } from "./WrapperComponent";

const Page = (props) => {
    return (
        <WrapperComponent activated={true} />
    );
};

するとこんな警告が発生します。
スクリーンショット 2021-03-25 20.36.37.png

警告内容
Warning: Received `true` for a non-boolean attribute `activated`.

エラー発生の理由

このエラーの発生の理由はstyled-componentsの公式FAQに記載されています。

The warning message below indicates that non-standard attributes are being attached to HTML DOM elements such as <div> or <a>.

この警告メッセージは標準ではない引数がHTMLのDOMに渡されたことを示しています。

この警告が発生しても動作に影響は起きませんが、できる限り不要なエラーは潰しておきたいものです。

エラーの対策

対策自体は簡単で、{...props}と受け取ったプロパティをそのまま<span>や<div>といった標準のHTML DOMに渡すのをやめれば警告は消えます。

修正後
import React from "react";
import styled, { css } from "styled-components";

const TextComponent = (props) => {
    return (
        // {...props}を削除
        <span className={props.className}>
            テキストテキストテキストテキスト
        </span>
    );
};

const WrapperComponent  = styled(TextComponent)`
    color: #999;
    font-size: 14px;

    ${(props) =>
        props.activated &&
        css`
            color: #000;
            font-weight: bold;
        `}
`;

エラーが表示されなくなりました。
スクリーンショット 2021-03-25 20.47.16.png

スタイリングするコンポーネントがライブラリのコンポーネントの場合

ただし、外部のコンポーネントを使用して、この警告に遭遇した場合には同じような対処はできません。
実は自分がこのWarningにハマったのも、FontAwesomeIconという外部のコンポーネントを使用したときでした。

使用例
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

const WrapperComponent  = styled(FontAwesomeIcon)`
    color: #999;
    font-size: 14px;

    ${(props) =>
        props.activated &&
        css`
            color: #000;
            font-weight: bold;
        `}
`;

この場合には、受け渡す値をboolean型からnumber型に変えることで警告が発生しなくなりますので、それで対応します。
(下記の記事参照)

対応後のPage.jsx
import React from "react";
import { WrapperComponent } from "./WrapperComponent";

const Page = (props) => {
    return (
        // +trueで1,+falseで0と評価される
        <WrapperComponent activated={+true} />
    );
};

※正直これで警告が消える理由はよく分かりません。Boolean型のときのみ例の警告が出るようです。

補足

今回のケースに限らず自作のコンポーネントの場合には、想定しないエラーが起きないよう、関数の引数に分割代入を使用して、コンポーネント内で利用する予定のものだけを割り当てるようにするべきでしょう。

分割代入を利用したFunctionComponentの作り方
const TextComponent = ({className}) => {
    return (
        <span className={className}>
            テキストテキストテキストテキスト
        </span>
    );
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動画ファイルを再生するReact-Player引っかかったところメモ

自分用メモ

windows10
wsl2:ubuntu
node: v15.9.0
react: 17.0.1
react-player:2.9.0

create-react-appして動画ファイルを使いたいときインストールしたのがReact-Player。

ubuntu
npm i -s react-player

で、実装。

App.js
import ReactPlayer from 'react-player'

function App() {
    return (
        <div>
           <ReactPlayer url='videos/〇〇.mp4' playing width="100%" loop/>
        </div>
    )
}
export default App

この時パスわかんなくなってハマりました。
react-playerではpublicフォルダの中を見ているようで、public以下のパスを記述して見れるようになりました。以上。

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

[Oracle Cloud] Oracle Content and Experience を Headless CMS として利用する React サンプルを動かしてみた

はじめに

Oracle Content and Experience (以降OCE)は、APIファーストなアーキテクチャで、マルチチャネルでのコンテンツ配信を実現するインテリジェントなコンテンツ管理プラットフォームです。

OCEの製品ドキュメントページには、OCEをヘッドレスコンテンツ管理システム(CMS)として使用するための、さまざまな開発フレームワークのサンプルが公開されてます

ここでは、ReactBuild a blog のサンプルを動かすことで、OCEをヘッドレスCMSとして利用するイメージを理解します

image.png

ここで紹介するチュートリアルを最後まで実施すると、以下のようなReactベースのブログサイトが表示されます。

  • トップページ
    image.png

  • カテゴリページ
    image.png

  • 記事詳細ページ
    image.png

準備

このチュートリアルを実施するには、以下の準備作業が必要です

  • Windows or MacOS が稼働するローカルマシン(ここではMacを利用)
  • Oracle Cloud アカウントの取得
  • OCEインスタンスの作成
  • 以下のアプリケーションロールが付与されたOCEユーザー
    • CECEnterpriseUser
    • CECContentAdministrator
    • CECRepositoryAdministrator
    • CECServiceAdministrator

公開済みのOCEチュートリアルを参考に、準備を進めてください

また、以下のチュートリアルを事前に実施しておくと、OCEをヘッドレスCMSとして利用するための理解が、さらに深まります。時間がありましたら、こちらも実施いただければ幸いです

ステップ1: OCEインスタンスにデモコンテンツをインポートし、公開する

参考マニュアル: Step 1: Prepare Oracle Content and Experience

1-1: OCEインスタンスにコンテンツを登録するリポジトリと公開チャネルを定義する

コンテンツを登録するリポジトリと、その登録されたコンテンツを公開する公開チャネルを定義します

  1. OCEインスタンスにサインインします

  2. 公開チャネルを作成します。ADMINISTRATION:コンテンツ→チャネルのパブリッシュを選択し、作成をクリックします
    001.png

  3. 以下の通りに入力し、保存をクリックします

    • 名前: OCEGettingStartedChannel
    • 公開チャネルのポリシー
      • アクセス: パブリック
      • 公開中: すべて公開できます 002.png
  4. 続けてリポジトリを作成します。ADMINISTRATION:コンテンツ→リポジトリを選択し、作成をクリックします
    003.png

  5. リポジトリを作成します。以下の通りに入力し、保存をクリックします

    • 名前: OCEGettingStartedRepository
    • 公開チャネル: OCEGettingStartedChannel
    • 言語
      • デフォルト言語: English (United States) (en-US) 004.png

1-2: コンテンツ・タイプの定義とコンテンツのインポート

アセット・リポジトリで利用するコンテンツ・タイプを定義し、その定義したコンテンツ・タイプから作成されるコンテンツ・アイテムおよびデジタル・アセットを登録します。

ここでは、Oracleより提供されるOCE Samples Asset Packを利用し、上記2つの作業をまとめて実施します。

  1. こちらのURLをクリックし、OCE Samples Asset Pack (OCESamplesAssetPack.zip)をダウンロードします

  2. ダウンロードしたOCESamplesAssetPack.zipを、ローカル環境上で展開(解凍)します。展開したディレクトリに OCEGettingStarted_data.zip があることを確認します
    image.png

  3. OCEインスタンスにサインインし、ADMINISTRATION:コンテンツ→リポジトリを開きます

  4. OCEGettingStartedRepositoryを選択し、コンテンツのインポートをクリックします
    image.png

  5. ファイルの選択画面が開きます。任意のフォルダ(ここではサンプルアセットフォルダ)に先ほど確認したOCEGettingStarted_data.zipをアップロードします。

  6. アップロードしたOCEGettingStarted_data.zipを選択し、OKをクリックします
    image.png

  7. コンテンツのインポートダイアログが表示されます。新規バージョンを追加して既存のアセットを更新を選択し、インポートをクリックします
    image.png

  8. コンテンツのインポート処理が実行されます。

  9. コンテンツ・パッケージOCEGettingStarted_data.zipを正常にインポートしました。が表示されることを確認し、OCEGettingStartedRepositoryを開きます。アセット・タイプに5つのアセット・タイプが登録されていることを確認し、右上の取消をクリックします
    image.png

    [Memo]

    Imageは事前定義済のアセット・タイプ(シードされたアセット・タイプ)で、jpgやpngなどの画像ファイルを管理します。
    OCEGettingStarted...で始まる残り4つのアセットタイプは、今回のReactサンプルで利用するコンテンツ・タイプの定義です。それぞれの定義については、こちらでご確認ください

  10. 左ナビゲーションメニューのアセットをクリックし、OCEGettingStartedRepositoryを選択します。アセットが登録されていることを確認します
    image.png

1-3:コンテンツの公開

リポジトリにインポートされたすべてのアセットを公開します

  1. OCEGettingStartedRepositoryで、すべて選択のチェックボックスを選択し、公開をクリックします
    image.png

  2. チャネルで選択済を選択し、公開用のチャネルの選択でOCEGettingStartedChannelを選択します。検証をクリックします
    image.png

  3. 検証が実行されます。合計問題数が0件であること確認し、右上の公開をクリックします
    image.png

  4. すべてのアセットが公開され、アセットのステータスが公開済になります
    image.png

ステップ2: React でブログサイトを作成する

参考マニュアル: Step 2: Build the Blog in React

Reactでブログサイトを作成します。サイト内で利用するコンテンツは、ステップ1で公開したアセットを利用します

2-1: ReactブログサンプルのGitHubリポジトリをローカルにクローンする

ReactブログサンプルをGithubリポジトリよりクローンします

git clone https://github.com/oracle/oce-react-blog-sample.git
cd oce-react-blog-sample

インストールします

npm install

2-2: Reactアプリケーションの構成

Reactアプリケーションが利用するOCEインスタンスの情報は、クローンしたoce-react-blog-sampleディレクトリ配下の.envファイルに記述します。.envファイルの内容は以下の通りです

#
# Copyright (c) 2020, 2021 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
#

# The connection details for the OCE server to be used for this application
SERVER_URL=https://samples.mycontentdemo.com
API_VERSION=v1.1
CHANNEL_TOKEN=47c9fb78774d4485bc7090bf7b955632

# The port the Express Server is to run on
EXPRESS_SERVER_PORT=8080
  • 説明
    • SERVER_URL: OCEインスタンスのURL。URLの形式はhttps://<oceinstance>-<tenancy>.cec.ocp.oraclecloud.com
    • API_VERSION: バージョン名。現在はv1.1
    • CHANNEL_TOKEN: OCEGettingStartedChannelのチャネルトークン
    • EXPRESS_SERVER_PORT: Reactアプリケーションのポート番号

.envファイルをエディタ等で開き、SERVER_URLCHANNEL_TOKENを、今回利用するOCEインスタンスのURLとチャネルトークンに書き換え、保存します

[Memo]
OCEGettingStartedChannelのチャネルトークンは、ADMINISTRATION:コンテンツ→チャネルのパブリッシュ→OCEGettingStartedChannelより確認します
image.png

2-3: Reactブログサンプルアプリケーションについて

Reactブログサンプルでは、OCEが提供するContent SDKを利用します。Content SDK の紹介、およびReactブログサンプルアプリケーションの解説は、以下のドキュメントをご確認ください

ステップ3:Reactアプリケーションを実行する

参考マニュアル: Step 3: Prepare Your Application for Deployment

3-1: Reactサンプルブログアプリケーションのビルドと実行

Reactサンプルブログアプリケーションをビルドし、実行します。

Development

npm run dev

Prodction

npm run build
npm start 

実行時の Listening on port XXXX でポート番号を確認します(もしくは、.envファイルを開き、ポート番号を確認)

3-2: Reactブログサンプルサイトの確認

  1. ブラウザで http://localhost:<ポート番号>を開くと、以下のようなサイトが確認できます
    image.png

  2. How Toをクリックすると、How Toカテゴリの記事2件が表示されます
    image.png

  3. How Toカテゴリ内のCreate Beautiful Latte Art!をクリックします。記事の詳細ページが表示されます
    image.png

3-3: アセットの更新

前の手順で確認した Create Beautiful Latte Art! の記事は、OCEの OCEGettingStartedRepositoryのアセット(コンテンツ・アイテム)として管理されています。ここではこの記事を編集・公開すると、Reactアプリケーション側も更新されることを確認します

  1. OCEにサインインし、アセット→OCEGettingStartedRepositoryを開きます

  2. 検索ボックスにCreate Beautiful Latte Art!と入力し、検索を実行します

  3. Create Beautiful Latte Art!を選択し、編集をクリックします
    image.png

  4. コンテンツ編集画面が開きます。Content Item Data FieldsのArticle Contentを編集します。編集後は、右上の保存をクリックします

image.png

  1. バージョンがv1.1が作成され、ステータスがドラフトに設定されます。メニューより公開をクリックします
    image.png

  2. 検証結果が表示されます。問題件数が0件であることを確認し、公開をクリックします
    image.png

  3. Create Beautiful Latte Art!が公開され、バージョンv2となります。またステータスが公開中に更新されます
    image.png

  4. 別ブラウザでhttp://localhost:8080を開きHow To→Create Beautiful Latte Art!をクリックします。先ほど編集した内容が表示されることを確認します

image.png

おわりに

最後までご確認いただきありがとうございました。

今回はReactのブログサンプルの使いかたを紹介しました。そのほかにもBuild an image galleryBuild a minimal siteのチュートリアルも公開されています。また、React 以外にもOracle JETJavaScriptのサンプルも公開されています。

興味がありましたら、お試しいただければ幸いです。サンプルが公開されているURLを再掲いたします

ちなみに、Oracle JET サンプルの使いかたは、Build your blog site using the Oracle JET framework and Oracle Content & Experienceのブログ(英語)で紹介されています。よろしければご活用ください。

以上でこのチュートリアルは終了です。引き続きOracle Content and Experience (OCE)をよろしくお願いいたします

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

Storybook for ReactをGitHub Pagesに公開する

Storybook for Reactで、TypeScriptとSass(Dart Sass)を使えるようにして、GitHub Pagesで手軽に公開できるようにします。

create-react-appのインストール

Storybook for Reactを使うためにはReactの環境が必要なので、Reactの環境構築ツールCreate React Appをインストールします。

terminal
npm install -g create-react-app

プロジェクト作成

terminal
//TypeScriptのテンプレートを使いたいので、--teplate typescriptオプションを付けます
npx create-react-app プロジェクト名 --template typescript

//インストールしたプロジェクトのフォルダ内に移動します。
cd プロジェクト名

//アプリを起動します。
npm start

ブラウザが起動して、Reactのロゴと、「Edit src/App.tsx and save to reload.」が表示されたら成功です。
--teplate typescriptオプションを付けてnpx create-react-appしたので、/src/App.jsxが/src/App.tsxとしてインストールされているはずです。

Sass(Dart Sass)の導入

Sassを使えるようにしていきます。

動作確認用のSCSSファイルの準備

/src/App.cssのファイル名を/src/App.scssに変更します。

忘れずにApp.scssで読み込んでいるCSSファイルをSCSSファイルに変更します。

/src/App.tsx
import "./App.css";
↓
import "./App.scss";

この時点ではまだSCSSファイルが読み込めないので、エラーになります。

Sassのインストール

アプリを終了させて、Sassをインストールします。
※node-sassだと現時点ではまだ@useが使えないようなので、他のscssファイルからの読み込みをする時に廃止予定の@importを使うことになるのでDart Sassを使います。

terminal
npm i -D sass 

動作確認

再度、アプリを起動します。

terminal
npm start

ブラウザが起動して、Reactのロゴと、「Edit src/App.tsx and save to reload.」が表示されたら成功です。

Storybookの導入

アプリを終了させて、Storybookをインストールします。

terminal
npx sb init

srcフォルダ配下に/storiesフォルダが作られるので、この中にコンポーネントを作成していくことになります。

動作確認

Storybookの起動

terminal
npm run storybook

ブラウザが起動してStorybookの画面が表示されれば成功です。

テーマ

Storybookの色や角丸、左上のロゴが変更できます。

パッケージのインストール

terminal
npm i -D @storybook/addons
npm i -D @storybook/theming

テーマファイルの作成

ファイル名は任意です。
色や数値、画像を変更して好きなデザインにしていきます。

.storybook\YourTheme.js
//Storybook Themingのインポート
import { create } from "@storybook/theming";

//ロゴ用の画像をインポート
import brandLogo from "../src/stories/assets/stackalt.svg";

export default create({
  //ベースにするデフォルトテーマ["light","dark"]
  base: "light",

  //不明
  colorPrimary: "hotpink",
  //左カラムメニューの選択色
  colorSecondary: "deepskyblue",

  // UI
  //背景色
  appBg: "white",
  //コンテンツ部分の背景色
  appContentBg: "silver",
  //ボーダー色
  appBorderColor: "grey",
  //角丸(ツールバーのメニュー選択時の小さいウィンドウ)
  appBorderRadius: 4,

  // Typography
  //フォント
  fontBase: '"Open Sans", sans-serif',
  fontCode: "monospace",

  // Text colors
  //文字色
  textColor: "black",
  //不明
  textInverseColor: "rgba(255,255,255,0.9)",

  // Toolbar default and active colors
  //ツールバーの文字色
  barTextColor: "silver",
  //ツールバーの選択されたメニューの文字色
  barSelectedColor: "black",
  //ツールバーの背景色
  barBg: "hotpink",

  // Form colors
  //inputの背景色
  inputBg: "white",
  //inputのボーダー色
  inputBorder: "silver",
  //input内の文字色
  inputTextColor: "black",
  //inputの角丸
  inputBorderRadius: 4,

  //ロゴのalt
  brandTitle: "My custom storybook",
  //ロゴのリンクURL
  brandUrl: "https://example.com",
  //ロゴ画像
  brandImage: brandLogo,
});

テーマの適用

manager.jsを新規に作成します。

.storybook\manager.js
import { addons } from "@storybook/addons";
import yourTheme from "./YourTheme";

addons.setConfig({
  theme: yourTheme,
});

設定変更

テーマ調整時は、キャッシュを効かなくすると調整しやすくなります。

package.json
"scripts": {
  //--no-manager-cacheオプションの追記
  "storybook": "start-storybook -p 6006 -s public --no-manager-cache",
},

動作確認

storybookの起動

terminal
npm run storybook

ブラウザが起動してStorybookのデザインが変更されていれば成功です。

Githubにpushする

リポジトリ作成

GitHubにアクセスして、公開用のリポジトリを作成します。
公開目的なので、Publicで作成します。

GitHub Pagesとしての公開URLは下記になりますので、それを考慮して命名します。
https://アカウント名.github.io/リポジトリ名/

ブランチ名変更

現在、branch名はmasterになっていると思いますが、Githubではmainを使うようになっているので、変更します。

terminal
//現在のブランチ名を確認
git branch

//現在のブランチ名をmainに変更する
git branch -m main

remote登録

作成したリポジトリのURLをコピーしておきます。

terminal
//GitHubに作成しリポジトリをリモートリポジトリoriginとして登録します。
git remote add origin https://github.com/アカウント名/リポジトリ名.git

//登録されたか確認します。
git remote -v

push

terminal
//ステージング
git add .

//コミット
git commit -m "コミットメッセージ"

//push
git push origin main

GitHub Pagesに公開する

gh-pagesのインストール

GitHubでgh-pagesブランチにpushすると、GitHub Pagesとして公開されるのをコマンド1回で実行できます。

terminal
npm  i -D gh-pages

package.jsonの追記

package.jsonにコマンドを追加します。

コマンド 内容
predelay deployコマンド実行前に、自動的にStorybookのビルドを実行
deploy storybook-staticフォルダの内容をgh-pagesブランチにpushする
package.json
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "storybook": "start-storybook -p 6006 -s public",
  "build-storybook": "build-storybook -s public",


  //追加
  "predeploy": "build-storybook",
  //追加
  "deploy": "gh-pages -d storybook-static"
}, 

.gitignoreの追記

ビルドに使われるstorybook-staticフォルダをGit管理から外したい場合は、.gitignoreに追記します。

.gitignore
/storybook-static

GitHub Pagesへのデプロイ実行

terminal
npm run deploy

表示確認

下記URLにアクセスしてStorybookが表示されれば完了です。

https://アカウント名.github.io/リポジトリ名/

addonの追加

必要に応じてアドオンを追加します。
https://storybook.js.org/addons

リポジトリ

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

Reactのstate hookに値を変更した配列を渡してもre-renderされなかった

タイトル通りです。
JSのArrayは参照渡しということがわかっていなかったのと、ReactのuseStateの理解が浅かったことが原因で若干時間をとってしまいました。

起こったこと

(コンポーネントの設計が色々とアレなのは許してください...)

一覧表示画面を実装していました。

//色々略

const App = () => {
    //略

    const [blogs, setBlogs] = useState([])

    //略

    return (
        <div>
            {/*略*/}

            {user && <BlogList blogs={blogs} setBlogs={setBlogs} />}
        </div>
    )
}

export default App;

↑のBlogListというのが一覧です。中身はこうなっています

const BlogList = ({ blogs, setBlogs}) => {
    return (
        <ul className="blog_ul">
            {blogs.map(blog =>
                <Blog key={blog.id} blog={blog} blogList={blogs} setBlogs={setBlogs} />
            )}
        </ul>
    )
}

Appコンポーネントからblogsステートとblogsを更新するためのsetBlogsをpropsとして受け取り、さらにリスト要素のBlogコンポーネントに渡しています。こういうバケツリレーをやっていいのかやるべきでないのか正直自信ないですが、とりあえずこうなってます。
Blogコンポーネントの中身は以下の通りです。

const Blog = ({blog, blogList, setBlogs}) => {
    const [showDetail, setShowDetail] = useState(false)

    const toggleDetail = () => {
        setShowDetail(!showDetail)
    }

    const incrementLike = async blog => {
        const currentBlogList = blogList
        const targetBlogId = blog.id
        const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)

        blog.likes = blog.likes + 1
        const updatedBlog = await blogService.like(blog)

        currentBlogList[targetBlogIndex] = updatedBlog
        setBlogs(currentBlogList)
    }

    const blogBrief = () => (
        <>
            {blog.title} {blog.author}
            <button onClick={toggleDetail}>view</button>
        </>
    )

    const blogDetail = () => (
        <>
            {blog.title} <button onClick={toggleDetail}>hide</button><br />
            {blog.url}<br />
            likes {blog.likes} <button onClick={() => incrementLike(blog)}>like</button><br />
            {blog.author}<br />
        </>
    )

    return (
        <li className="blog_style">
            {showDetail ? blogDetail() : blogBrief()}
        </li>
    )
}

色々書いてありますが、今回問題が起こったのは incrementLike の関数でした。
この関数はblogのlikeの値をインクリメントする関数で、画面上だと "like" のボタンをクリックすることで発火します。
サーバーから更新されたblogオブジェクトが返ってきたらblogList内の該当する要素と置き換えて、setBlogs関数にその配列を渡します。そうすることでblogsが更新されて画面がre-renderされ、画面上のlikeの数が変わるというわけです。

likeボタンを押すと確かにlikeの値はしっかり変更されるのですが、画面上ではlikeの数はそのままでした。
つまり、re-renderされていませんでした。

一旦デバッグしてみる

とりあえずconsole.logしまくります。

const incrementLike = async blog => {
    const currentBlogList = blogList
    console.log('current blogs:',currentBlogList)
    const targetBlogId = blog.id
    console.log('target blog id:',targetBlogId)
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
    console.log('targ blog index:', targetBlogIndex)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)
    console.log('updated blog:',updatedBlog)

    currentBlogList[targetBlogIndex] = updatedBlog
    setBlogs(currentBlogList)
}

で、この要素を更新してみます
スクリーンショット 2021-03-24 23.31.57.png
現在likesの値は6なので、一度likeをクリックすれば7になるはずです。

スクリーンショット 2021-03-24 23.54.44.png

ちゃんと7になってます。
ただ、なんか元の配列内の要素も同じく更新されてしまっています。参照渡しになってるっぽい?
意図していた挙動ではなかったので一旦Arrayでググってみる。

Arrays are a special type of objects. The typeof operator in JavaScript returns "object" for arrays.

ArrayはObjectらしいです。

Objectということは参照渡しです。つまり、BlogコンポーネントのincrementLikeでcurrentBlogListを更新するということは、もとを辿ってゆくとAppコンポーネントのblogsを更新しているのと同じということです。
ただ、同じ値であったとしてもblogsを更新するsetBlogsに値を渡しているのだからre-renderされるのでは? という考えが払拭できなかったので、とりあえず公式ドキュメントを読み直してみました。

同じ値で更新を行った場合re-renderされない

現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します(React は Object.is による比較アルゴリズムを使用します)。

らしいです。
つまり

const incrementLike = async blog => {
    const currentBlogList = blogList
    console.log('current blogs:',currentBlogList)
    const targetBlogId = blog.id
    console.log('target blog id:',targetBlogId)
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
    console.log('targ blog index:', targetBlogIndex)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)
    console.log('updated blog:',updatedBlog)

    currentBlogList[targetBlogIndex] = updatedBlog
    console.log('are blogList and currentBloglist the same object?:', Object.is(blogList, currentBlogList))
    setBlogs(currentBlogList)
}


スクリーンショット 2021-03-25 0.29.00.png

こういうことなので画面はそのままだったということのようです。

修正

const incrementLike = async blog => {
    const currentBlogList = [...blogList]
    const targetBlogId = blog.id
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)

    currentBlogList[targetBlogIndex] = updatedBlog
    setBlogs(currentBlogList)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む