- 投稿日:2021-03-25T23:52:48+09:00
フロントエンド開発者のための刺激的なプロジェクト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 外に移動したときに発行されます。
mouseenter
とmouseleave
はある要素内へのカーソルの出入りを検知し、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
を呼び出している。つづく
コードみたら何をしているのかは推測がつくのですが、このコードを作り上げる発想がすごいと思いました。
引き続きコード読み解いていきます。
- 投稿日:2021-03-25T21:18:47+09:00
styled-componentsで`Received "true" for a non-boolean attribute`というエラーが出たときの対処法
下記のようにstyled-componentsでactivatedというプロパティがtrueのときには
font-weight: bold;
になるようなコンポーネントを作りたいとします。
※記法はjsxを採用WrapperComponent.jsximport 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.jsximport React from "react"; import { WrapperComponent } from "./WrapperComponent"; const Page = (props) => { return ( <WrapperComponent activated={true} /> ); };警告内容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; `} `;スタイリングするコンポーネントがライブラリのコンポーネントの場合
ただし、外部のコンポーネントを使用して、この警告に遭遇した場合には同じような対処はできません。
実は自分がこの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.jsximport 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> ); };
- 投稿日:2021-03-25T19:22:55+09:00
動画ファイルを再生するReact-Player引っかかったところメモ
自分用メモ
windows10
wsl2:ubuntu
node: v15.9.0
react: 17.0.1
react-player:2.9.0create-react-appして動画ファイルを使いたいときインストールしたのがReact-Player。
ubuntunpm i -s react-playerで、実装。
App.jsimport ReactPlayer from 'react-player' function App() { return ( <div> <ReactPlayer url='videos/〇〇.mp4' playing width="100%" loop/> </div> ) } export default Appこの時パスわかんなくなってハマりました。
react-playerではpublicフォルダの中を見ているようで、public以下のパスを記述して見れるようになりました。以上。
- 投稿日:2021-03-25T19:09:53+09:00
[Oracle Cloud] Oracle Content and Experience を Headless CMS として利用する React サンプルを動かしてみた
はじめに
Oracle Content and Experience (以降OCE)は、APIファーストなアーキテクチャで、マルチチャネルでのコンテンツ配信を実現するインテリジェントなコンテンツ管理プラットフォームです。
OCEの製品ドキュメントページには、OCEをヘッドレスコンテンツ管理システム(CMS)として使用するための、さまざまな開発フレームワークのサンプルが公開されてます
ここでは、React の Build a blog のサンプルを動かすことで、OCEをヘッドレスCMSとして利用するイメージを理解します
ここで紹介するチュートリアルを最後まで実施すると、以下のようなReactベースのブログサイトが表示されます。
準備
このチュートリアルを実施するには、以下の準備作業が必要です
- 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インスタンスにコンテンツを登録するリポジトリと公開チャネルを定義する
コンテンツを登録するリポジトリと、その登録されたコンテンツを公開する公開チャネルを定義します
OCEインスタンスにサインインします
公開チャネルを作成します。ADMINISTRATION:コンテンツ→チャネルのパブリッシュを選択し、作成をクリックします
以下の通りに入力し、保存をクリックします
リポジトリを作成します。以下の通りに入力し、保存をクリックします
1-2: コンテンツ・タイプの定義とコンテンツのインポート
アセット・リポジトリで利用するコンテンツ・タイプを定義し、その定義したコンテンツ・タイプから作成されるコンテンツ・アイテムおよびデジタル・アセットを登録します。
ここでは、Oracleより提供されるOCE Samples Asset Packを利用し、上記2つの作業をまとめて実施します。
こちらのURLをクリックし、OCE Samples Asset Pack (OCESamplesAssetPack.zip)をダウンロードします
ダウンロードした
OCESamplesAssetPack.zip
を、ローカル環境上で展開(解凍)します。展開したディレクトリに OCEGettingStarted_data.zip があることを確認します
OCEインスタンスにサインインし、ADMINISTRATION:コンテンツ→リポジトリを開きます
ファイルの選択画面が開きます。任意のフォルダ(ここでは
サンプルアセット
フォルダ)に先ほど確認したOCEGettingStarted_data.zipをアップロードします。コンテンツのインポートダイアログが表示されます。新規バージョンを追加して既存のアセットを更新を選択し、インポートをクリックします
コンテンツのインポート処理が実行されます。
コンテンツ・パッケージOCEGettingStarted_data.zipを正常にインポートしました。
が表示されることを確認し、OCEGettingStartedRepositoryを開きます。アセット・タイプに5つのアセット・タイプが登録されていることを確認し、右上の取消をクリックします
[Memo]
Image
は事前定義済のアセット・タイプ(シードされたアセット・タイプ)で、jpgやpngなどの画像ファイルを管理します。
OCEGettingStarted...
で始まる残り4つのアセットタイプは、今回のReactサンプルで利用するコンテンツ・タイプの定義です。それぞれの定義については、こちらでご確認ください左ナビゲーションメニューのアセットをクリックし、OCEGettingStartedRepositoryを選択します。アセットが登録されていることを確認します
1-3:コンテンツの公開
リポジトリにインポートされたすべてのアセットを公開します
ステップ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 install2-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_URLとCHANNEL_TOKENを、今回利用するOCEインスタンスのURLとチャネルトークンに書き換え、保存します[Memo]
OCEGettingStartedChannelのチャネルトークンは、ADMINISTRATION:コンテンツ→チャネルのパブリッシュ→OCEGettingStartedChannelより確認します
2-3: Reactブログサンプルアプリケーションについて
Reactブログサンプルでは、OCEが提供するContent SDKを利用します。Content SDK の紹介、およびReactブログサンプルアプリケーションの解説は、以下のドキュメントをご確認ください
- Work with the OCE Content SDK
- Use the OCE Content SDK to Fetch Content
- React Blog Application Flow
- React Components
ステップ3:Reactアプリケーションを実行する
参考マニュアル: Step 3: Prepare Your Application for Deployment
3-1: Reactサンプルブログアプリケーションのビルドと実行
Reactサンプルブログアプリケーションをビルドし、実行します。
Development
npm run devProdction
npm run build npm start実行時の
Listening on port XXXX
でポート番号を確認します(もしくは、.env
ファイルを開き、ポート番号を確認)3-2: Reactブログサンプルサイトの確認
3-3: アセットの更新
前の手順で確認した
Create Beautiful Latte Art!
の記事は、OCEの OCEGettingStartedRepositoryのアセット(コンテンツ・アイテム)として管理されています。ここではこの記事を編集・公開すると、Reactアプリケーション側も更新されることを確認します
OCEにサインインし、アセット→OCEGettingStartedRepositoryを開きます
検索ボックスにCreate Beautiful Latte Art!と入力し、検索を実行します
コンテンツ編集画面が開きます。Content Item Data FieldsのArticle Contentを編集します。編集後は、右上の保存をクリックします
Create Beautiful Latte Art!
が公開され、バージョンv2
となります。またステータスが公開中
に更新されます
別ブラウザで
http://localhost:8080
を開きHow To→Create Beautiful Latte Art!をクリックします。先ほど編集した内容が表示されることを確認しますおわりに
最後までご確認いただきありがとうございました。
今回はReactのブログサンプルの使いかたを紹介しました。そのほかにもBuild an image galleryやBuild a minimal siteのチュートリアルも公開されています。また、React 以外にもOracle JETやJavaScriptのサンプルも公開されています。
興味がありましたら、お試しいただければ幸いです。サンプルが公開されているURLを再掲いたします
ちなみに、Oracle JET サンプルの使いかたは、Build your blog site using the Oracle JET framework and Oracle Content & Experienceのブログ(英語)で紹介されています。よろしければご活用ください。
以上でこのチュートリアルは終了です。引き続きOracle Content and Experience (OCE)をよろしくお願いいたします
- 投稿日:2021-03-25T16:10:19+09:00
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をインストールします。
terminalnpm 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.tsximport "./App.css"; ↓ import "./App.scss";この時点ではまだSCSSファイルが読み込めないので、エラーになります。
Sassのインストール
アプリを終了させて、Sassをインストールします。
※node-sassだと現時点ではまだ@useが使えないようなので、他のscssファイルからの読み込みをする時に廃止予定の@importを使うことになるのでDart Sassを使います。terminalnpm i -D sass動作確認
再度、アプリを起動します。
terminalnpm startブラウザが起動して、Reactのロゴと、「Edit src/App.tsx and save to reload.」が表示されたら成功です。
Storybookの導入
アプリを終了させて、Storybookをインストールします。
terminalnpx sb initsrcフォルダ配下に/storiesフォルダが作られるので、この中にコンポーネントを作成していくことになります。
動作確認
Storybookの起動
terminalnpm run storybookブラウザが起動してStorybookの画面が表示されれば成功です。
テーマ
Storybookの色や角丸、左上のロゴが変更できます。
パッケージのインストール
terminalnpm 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.jsimport { 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の起動
terminalnpm run storybookブラウザが起動してStorybookのデザインが変更されていれば成功です。
Githubにpushする
リポジトリ作成
GitHubにアクセスして、公開用のリポジトリを作成します。
公開目的なので、Publicで作成します。GitHub Pagesとしての公開URLは下記になりますので、それを考慮して命名します。
https://アカウント名.github.io/リポジトリ名/ブランチ名変更
現在、branch名はmasterになっていると思いますが、Githubではmainを使うようになっているので、変更します。
terminal//現在のブランチ名を確認 git branch //現在のブランチ名をmainに変更する git branch -m mainremote登録
作成したリポジトリのURLをコピーしておきます。
terminal//GitHubに作成しリポジトリをリモートリポジトリoriginとして登録します。 git remote add origin https://github.com/アカウント名/リポジトリ名.git //登録されたか確認します。 git remote -vpush
terminal//ステージング git add . //コミット git commit -m "コミットメッセージ" //push git push origin mainGitHub Pagesに公開する
gh-pagesのインストール
GitHubでgh-pagesブランチにpushすると、GitHub Pagesとして公開されるのをコマンド1回で実行できます。
terminalnpm i -D gh-pagespackage.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-staticGitHub Pagesへのデプロイ実行
terminalnpm run deploy表示確認
下記URLにアクセスしてStorybookが表示されれば完了です。
https://アカウント名.github.io/リポジトリ名/
addonの追加
必要に応じてアドオンを追加します。
https://storybook.js.org/addonsリポジトリ
- 投稿日:2021-03-25T00:41:14+09:00
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) }で、この要素を更新してみます
現在likesの値は6なので、一度likeをクリックすれば7になるはずです。↓
ちゃんと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) }こういうことなので画面はそのままだったということのようです。
修正
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) }