りゅーそう
元高校地歴科教員。現在microCMSでエンジニアをしています。
Saitama.jsというLT会を運営中です。
発言はすべて個人の意見です。
ReacttTutorial
2020/07/01
2022/01/23
お待たせしました!(?)
連載企画(?)の第一弾です。
これは当ブログのUIを作ってReactHooksのAPIを学んでいく企画です!
私自身もReactを触り始めて数ヶ月ほどの初心者に毛が生えたレベルですが、Reactを一から勉強するに当たって、「このReactの機能は実務でどう使うのか?」がイメージできずに苦労してきました。
Reactの機能を詳しく紹介していて分かりやすい記事はありがたいことにこの世にたくさんあるのですが、実際にUIやサービスを作りながらという記事はあまり見かけません。
私自身エンジニアではないということもあり、同じように実務でなかなかReactに触れる機会があまりない人のために記事を書いていこうと思いました。
とは言いつつも、自分のためでもあります。特にReactのv16.8~にでたReactHooksを用いてUIを作成していく力を身に付けたかったのでアウトプットも兼ねて連載をしていこうと思います。
以下の記事にインスピレーションを受けました。
React Hooksで作るGUI
というわけで、今回のテーマはuseStateです。
UIを作成しながら、useStateの使い方について学んでいきましょう。
UIはハンバーガーメニューを作成していきたいと思います。
当ページのスマホページではUIとしてハンバーガーメニューを使用しています。
ハンバーガーメニューをクリックすると、アニメーションが発火し、ドロワーが開く。
このUIは以下のページで紹介されているコードを参考にさせていただいています。
アクセシビリティにも配慮されたハンバーガーメニューになります。
ハンバーガーボタン 何で作ってる?僕なりの作り方を解説してみる。
詳しくは元ページを参照していただければと思いますが、簡単に仕様について説明します(当ブログのスマホページでぜひ実際にお試しください)。
ドロワーの開閉の状態を切り替える方法は様々な方法がありますが、上記のページを参考にして、aria-expandedの状態を切り替えることによってハンバーガーメニューの開閉を行なっています。
//buttonにaria-expanded属性を付与する
//今回作成するハンバーガーメニューのコンポーネント
return (
<>
<button
type="button"
className="button hamburger"
aria-controls="global-nav"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
<span className="hamburgerLine">
<span className="visuallyHidden">
メニューを開閉する
</span>
</span>
</button>
</>
);
//nav要素にaria-expanded属性を付与する
//今回作成するハンバーガーメニューを押した時に開くNavコンポーネント
return (
<nav aria-expanded={open}>
<ul>
<li>
<Link
to="/"
aria-label="HOME"
onClick={() => setOpen(!open)}
>
<ul>
<li className="NavListIcon">
<Img
fixed={data.file.childImageSharp.fixed}
alt="Logo"
/>
</li>
<li>HOME</li>
</ul>
</Link>
</li>
<li>
<Link
to="/about"
aria-label="ABOUT"
onClick={() => setOpen(!open)}
>
<ul>
<li className="NavListIcon">
<FaReact size={36} />
</li>
<li>ABOUT</li>
</ul>
</Link>
</li>
<li>
<Link
to="/posts"
aria-label="POSTS"
onClick={() => setOpen(!open)}
>
<ul>
<li className="NavListIcon">
<TiPencil size={36} />
</li>
<li>POST</li>
</ul>
</Link>
</li>
<li>
<Link
to="/works"
aria-label="WORK"
onClick={() => setOpen(!open)}
>
<ul>
<li className="NavListIcon">
<MdWork size={36} />
</li>
<li>WORK</li>
</ul>
</Link>
</li>
<li>
<Link
to="/contacts"
aria-label="CONTACTS"
onClick={() => setOpen(!open)}
>
<ul>
<li className="NavListIcon">
<FiMail size={36} />
</li>
<li>CONTACT</li>
</ul>
</Link>
</li>
</ul>
</nav>
);
)
逆に言えば上記のコードを読んで実装をイメージできた人は読む必要はあまりありません(React初心者の人にぜひ宣伝お願いします笑)。
ちなみに私はReactの静的サイトジェネレーターであるGatsbyを使用して実装していますが、Reactでも問題なく動作するかと思います(Navコンポーネント内のページネーションはReact Routerなどの実装が必要になるかと思いますが、当記事ではUIを作成するのが目的なのでそこには触れません)。
それでは実際にやっていきましょう。
プロジェクトをスタートさせます。
create-react-appやgatsby newなどで雛形を用意してください。
なお、サンプルコードはTypeScriptで書いています。
src
- components
- Atom
- Burger
index.tsx(ハンバーガーメニューのコンポーネント)
- Nav
index.tsx(Navメニューのコンポーネント)
- Layout
layout.tsx(Burger,Navコンポーネントをまとめるコンポーネント)
//Layoutファイルでルートのファイルをラップする
- App.js(create-react-appの場合)
- gatsby-browser.js(Gatsbyの場合)
AtomicDesignに一部沿ってフォルダを分割します。
このあたりは適当にファイル分割しています。Reactのコンポーネント指向というやつですが、私はそんなにカチカチにAtomicDesignの原則に添わずに小さいcomponentはAtomにぶっ込んで、ページを構成するコンポーネントはtemplatesに残りはぶっ込むという方針でフォルダを分けています。また、Layoutは別のフォルダにしています。
Layoutコンポーネントでアプリのルートをラップして、全てのファイルに適用されるようにしましょう。以下はgatsbyの例になります。
//gatsby-browser.js
import React from 'react';
import { Layout } from './src/components/layout';
export const wrapPageElement = ({ element }) => {
return <Layout>{element}</Layout>;
};
これで準備は完了です。
今回使用するuseStateについて紹介したいと思います。
公式:React-ステートフックの利用法
フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。
以下例です(公式を参照しました)。
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
例のように関数コンポーネントにstate(状態)を持たせたい時に使用します。
const [count, setCount] = useState(0);
useStateを宣言すると、2つの値が渡されるので鍵かっこ内に変数を宣言します。例だとカウンターなのでcountという変数を宣言しています。
useStateの引数である(0)は初期値を表します。今回は0を与えています。
呼び出す時は以下のように呼び出します。thisを使うことなく呼び出せるので直感的に記述できるのがポイントです。
<p>You clicked {count} times</p>
新たな値を与えたい時は、2つ目の値であるsetCountを呼び出します。例ではcountに+1を足して値を返しています。
<button onClick={() => setCount(count + 1)}>
このようにReact.useStateを使うと関数コンポーネント内でstateを管理することができるのでとてもスマートに書くことができます。
では実際にやっていきましょう。
前章で述べたように関数コンポーネントにStateを宣言していきます。まずは親コンポーネントであるLayout/index.tsxにState(状態)を宣言していきましょう。
//Layout/index.tsx
import React, { useState, ReactNode } from 'react';
import { Nav } from '../Atom';
import { Burger } from '../Atom';
type Props = {
children: ReactNode;
};
export const Layout: React.FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false);
return (
<>
<Burger open={open} setOpen={setOpen} />
<Nav open={open} setOpen={setOpen} />
<div>
<main>{children}</main>
</div>
</>
);
};
今回のサンプルコードはTypeScriptを用いています。ReactでTypeScriptを用いると例のようにPropsに型を与えることでpropsに型が適用されます。
type Props = {
children: ReactNode;
};
export const Layout: React.FC<Props> = ({ children }) => {
この例では、childrenにReactNodeという@type/reactが持っている型を渡しています。
このようにReactとTypeScriptの相性はとても良いので導入しない手はありません。
const [open, setOpen] = useState(false);
前章にしたがってuseStateを宣言します。ドロワーが開くので関数名はopenとしました。初期値には真偽値のfalseを入れます。使用通りこのStateがtrueに切り替わるのと同時にアニメーションするようにします。
Layoutコンポーネントからみて子コンポーネントにあたるBurger,NavコンポーネントにStateの値をします。
Burger,Navコンポーネントは後ほど作成します。
<Burger open={open} setOpen={setOpen} />
<Nav open={open} setOpen={setOpen} />
現在の状態は以下のようにopenとsetOpenという2つのstateが渡されている状態になります。
このようにstateを簡単に共有できます。
ハンバーガーメニューを表示するBurgerコンポーネントを作成していきます。button要素でUIを構成していきます。
ポイントはaria-expanded属性を要素に渡すことです。これによって要素が当たっていることが分かりやすくなります。
//Burger/index.tsx
import React from 'react';
type Props = {
open: boolean;
setOpen: Function;
};
export const Burger: React.FC<Props> = ({
open,
setOpen,
}) => {
return (
<>
<div css={hamburger}>
<button
type="button"
className="button hamburger"
aria-controls="global-nav"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
<span className="hamburgerLine">
<span className="visuallyHidden">
メニューを開閉する
</span>
</span>
</button>
</div>
</>
);
};
ReactとTypeScriptの相性はとても良いです。このように型をPropsに渡すことによって、どのような値を受け取っているのか明示的に書くことができます。
Layoutコンポーネントから受け取ったopen,setOpenを受け取ります。
openにはboolean,setOpenは関数的な使い方をするのでTypeScriptが持っているFunction型をここでは使用しています。
type Props = {
open: boolean;
setOpen: Function;
};
//Layoutコンポーネントのstateを受け取る。
export const Burger: React.FC<Props> = ({
open,
setOpen,
}) => {
button要素のaria-expandedにopenの値を当てることによって、スタイルを変化させます。
return (
<>
<div css={hamburger}>
<button
type="button"
className="button hamburger"
aria-controls="global-nav"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
<span className="hamburgerLine">
<span className="visuallyHidden">
メニューを開閉する
</span>
</span>
</button>
</div>
</>
);
このようにopenの値を受け取ります。LayoutコンポーネントのStateを受け取っているので、初期値はfalseになります。
//openの値はfalse
aria-expanded={open}
メソッドonClickを当てることによって、ハンバーガーメニューがクリックされた時に真偽値を切り替えます。
//クリックされると真偽値が逆になる。falseの場合trueに切り替わる
onClick={() => setOpen(!open)}
これで状態を切り替えることができるようになりました。
あと以下のようにCSSでアニメーションを設定します。
.hamburger {
//デフォルトのスタイルを当てる
}
.hamburger[aria-expanded='true'] {
//aria-expandedがtrueになった際に発火させたいスタイルを当てる
}
詳しいスタイルはCSSinJSのemotionで書いたものになりますが、以下のリポジトリを参照してください。
GitHub | PortfolioSite
これでuseStateを使ってハンバーガーメニューのアニメーションを実装することができました。
次にハンバーガーメニューをクリックすると展開されるNavコンポーネントを作成していきましょう。
Navコンポーネントが展開される仕組みについてまず説明します。
先ほどBurgerコンポーネントにて状態を切り替えるonClickイベントを実装しました。Burgerコンポーネントで切り替わった状態は親コンポーネントであるLayoutで共有されているのでNavコンポーネントにも共有されます。
このように少ないコンポーネント間でstateを管理するのにuseStateは適したHooksです。
よって、Layoutコンポーネントから状態を受け取れば実装は完了です。
import React from 'react';
type Props = {
open: boolean;
setOpen: Function;
};
export const Nav: React.FC<Props> = ({ open, setOpen }) => {
return (
<div>
<nav aria-expanded={open}>
<ul>
<li>HOME</li>
<li>ABOUT</li>
<li>POST</li>
<li>WORK</li>
<li>CONTACT</li>
</ul>
</nav>
</div>
);
};
先ほどのBurgerコンポーネント同様に、aria-expandedの状態によってスタイルを切り替えます。
nav {
//デフォルトのスタイルを当てる
}
nav [aria-expanded='true'] {
//aria-expandedがtrueになった際に発火させたいスタイルを当てる
}
詳しいスタイルは以下をご覧ください。
GitHub | PortfolioSite
これでハンバーガーメニューをクリックするとstateが切り替わり、ドロワーが開閉するようになりました!
いかがでしたでしょうか?
ReactHooksのuseStateを使えば、関数コンポーネントで簡単に状態管理を行うことができます。
HooksとCSSでアニメーションの切り替えを行うと、実装もシンプルにすみます。アニメーションはCSSに責務が分かれますので、コードもみやすいものになるのではないかと思います。
React ハンバーガーメニューで検索するとCSSinJSに状態の切り替えを埋め込んで、実装するコードがヒットしましたが、このようにCSSライクに書くと処理が明確になって良いのではないでしょうか?
ぜひ、試してみてください。
次回はuseEffectを使って何か作ってみたいと思います(ブログで実装できるUIネタを募集しています。)。
最後まで読んでくださり、ありがとうございました。何か表現など誤りがございましたらCONTACTフォームがtwitterまでコメントください。