以前、React初学者向けの勉強会を開催したときに作った資料を、Qiita向けに調整したものです。
- Reactの初歩的な記法
- Next.jsでのサーバーサイドレンダリングの概要
- CSSinJSの使い方
を学ぶことができます。
事前にNode.jsをインストールしている必要があります。
Reactとは?
Reactとは、Facebookが作ったJavaScriptライブラリです。ユーザーインターフェイスをコンポーネントベースで作ることができます。
公式サイト:https://ja.reactjs.org/
シンプルなReactのサンプル
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- babelがJSXをReact.createElement()に変換してくれる -->
<script type="text/babel">
/**
* 引数:propsを、JSX:<div>{props.text}</div>で受け取り、returnで返す。
* この一式をコンポーネントと呼ぶ。
*/
function Hello(props) {
return <div>{props.text}</div>
};
/**
* Helloコンポーネントを<div id="root"></div>にマウントしている。
* 関数みたいにしてみると、`Hello({ text: 'Hello, React!' });` こんな感じ。
*/
ReactDOM.render(
<Hello text="Hello, React!" />,
document.getElementById("root")
);
</script>
</body>
</html>
Reactの特徴
Vue.jsがディレクティブを使いHTMLを拡張するような方法で開発するのに対し、**ReactはガシガシJavaScriptを書いていきます。**まあ、Vue.jsもガッツリ開発を始めるとガシガシJavaScriptを書くことになると思いますが(・ω・)
また、Reactは基本的にデータを受け取って適切なViewを返すことを目的としたシンプルなライブラリなので、Angularのようなルールはなく、自由度がかなり高いです。逆に言うと、しっかりとした設計ができてないと、開発途中でつらくなります(・ω・)
宣言的UI
Reactに限らず、近年のUIフレームワーク・ライブラリ、およびプログラミングにおいて主要なパラダイムである宣言的UIについて、知っておく必要があります。
そのまえに、まず、命令的と宣言的を解説します。
命令的
- 何をするかを記述する
- 前回の実行結果に依存する
- 変数の再代入が行われる
宣言的
- どういう状態になるのかを記述する
- 前回の実行結果に依存しない
- 変数に再代入しない
命令的UIと宣言的UI
命令的UI
命令的UIの例として、jQueryによるDOM操作があげられます。
<!-- この時点ではUIの最終的な状態はわからない -->
<ul id="list"></ul>
const animals = ["ねずみ", "うし", "とら"];
// 配列の要素分処理を繰り返し、HTML側に挿入することでUIが決定する。
animals.forEach(animal => {
$('#list').append(`<li>${animal}</li>`);
});
宣言的UI
一方で、Vue.jsは宣言的にUIを作ることができます。
<template>
<ul>
<!-- この時点でUIの状態が決まっている -->
<li v-for="(item, index) in list" :key="index">{{item}}</li>
</ul>
</template>
<script>
export default {
data() {
return {
// 配列の要素によってリストの数が決定する
list: ["ねずみ", "うし", "とら"]
};
}
};
</script>
ReactやVue.jsは、jQueryの次に流行っているフレームワーク・ライブラリではなく、宣言的なUIを作るためのフレームワーク・ライブラリです。
技術選定時に宣言的なUIが必要であれば、ReactやVue.jsを使用しましょう。逆に言えば、jQueryのほうが適切な場面であれば、無理に使用する必要はありません。
Next.jsとは?
Next.jsとは、Reactでサーバーサイドレンダリングをするためのフレームワークです。Vue.jsで言うところの、Nuxt.js。簡単にルーティングできて、静的サイトの書き出しもできます。
公式サイト:https://nextjs.org/
静的サイトの書き出しならば、Gatsby.jsのほうが使いやすいかもしれませんが、Next.jsのほうがシンプルに始められるので、今回はNext.jsを使用します。
Reactとしての書き方はほぼ同じですし、メジャーなエコシステムも使用できるなので、Next.jsが使えればGatsby.jsも使えると思います。多分。
サーバーサイドレンダリングとは?
サーバーサイドレンダリングとは、PHPやRuby、Javaのように、サーバーサイドでDOMを生成してクライアントに静的なHTMLとして渡すことです。
Reactは、本来クライアントサイドで仮想DOMを生成し、それを実DOMとしてブラウザに描画します。
<!-- サーバーから返されるHTML -->
<div id="app"></div>
import React from 'react';
import ReactDOM from "react-dom";
// クライアントサイドでDOMを書き換える
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
サーバーサイドレンダリングは、サーバー上でReactを実行し、生成したDOMをクライアントへ渡します。
たとえば、動的なコンテンツで<title>
要素や<meta>
要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。
しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。
Next.jsの使い方
必要なパッケージをインストール
- next
- react
- react-dom
$ mkdir nextjs-sample
$ cd nextjs-sample
$ npm init -y
$ npm i next react react-dom
package.json
にscripts
を追記。
{
"scripts": {
"dev": "next"
}
}
./pages
ディレクトリにindex.js
を追加。
$ mkdir pages
$ touch pages/index.js
// pages/index.js
export default () => <h1>Hello, Next.js!</h1>
ローカルサーバーを起動。
$ npm run dev
http://localhost:3000
にアクセスして、Hello, Next.js!
が表示されていれば、OK!
ディレクトリ構成
-
./pages
ルーティングの対象 -
./static
静的ファイルの置き場所- 画像ファイルとか
ディレクトリのルールが決まっているのは、これくらい。
また、Next.js 9.1からsrc
配下でも利用できるようになったので、以下でもOKです。
-
src/pages
ルーティングの対象 -
src/static
静的ファイルの置き場所
JSX
JSXを使用すると、JavaScript上でHTMLのような構文が使えます。
// JSX
const Button = <button className="my-button">ボタン</button>
これは、React.createElement()
の糖衣構文で、JSXを使わないと下記の記述になります。
const Button = React.createElement("button", {
className: "my-button"
}, "ボタン");
極論、JSXを使わずにReact.createElement()
を使ってもなんの問題ありません。
JSXを使う理由が公式のガイドにありますので、興味のある方はどうぞ。
オンライン Babel コンパイラを使うと、JSXがどのようなJavaScriptに変換されるのかを確認できます。
Next.js(React)を書いてみよう
pages/index.js
を、省略形なしの形に変更。
import React from 'react' // Next.jsでは省略可能
// returnでJSXを返す関数をコンポーネントと呼ぶ
function Index() {
return <h1>Hello, Next.js!</h1>
}
// ES Modules
// 本来は import されて react-dom がレンダリングするが、Next.jsでは隠蔽されている
export default Index
HTMLのように、JSXでも子要素を使うことができます。
import React from 'react'
function Index() {
// ()で括り、;の自動挿入に対応
// returnで返すJSXは必ず1つの要素
return (
<div>
<h1>Hello, Next.js!</h1>
</div>
)
}
export default Index
JSXは{}
でJavaScriptを使うことができます。
import React from 'react'
function Index() {
const text = 'Next.js!'
return (
<div>
{/* コメントアウト */}
<h1>{`Hello, ${text}`}</h1>
</div>
)
}
export default Index
Heading
コンポーネントを作って、JSX内で使ってみましょう。
import React from 'react'
// 見出し用のコンポーネント
function Heading(props) {
// 属性の値は、オブジェクトのプロパティとして渡される
return <h1>{props.text}</h1>
}
function Index() {
const text = 'Next.js!'
return (
<div>
{
/**
* コンポーネントの属性でテキストを渡す
* これをProps(プロップス)と呼ぶ
*/
}
<Heading text={`Hello, ${text}`} />
</div>
)
}
export default Index
Heading
コンポーネントに、子要素を渡してみます。
import React from 'react'
// Propsはオブジェクトなので、分割代入が使える
function Heading({ children }) {
// childrenで子要素を受け取る
return <h1>{children}</h1>
}
function Index() {
const text = 'Next.js!'
return (
<div>
{/* コンポーネントの子要素でspan要素を渡す */}
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
</div>
)
}
export default Index
div
がいらねえときは、React.Fragment
が便利です。
import React from 'react'
function Heading({ children }) {
return <h1>{children}</h1>
}
function Index() {
const text = 'Next.js!'
// React.Fragmentを使うとその要素はレンダリングされない
return (
<React.Fragment>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<p>divでラップしたないねん</p>
</React.Fragment>
)
}
export default Index
React.Fragment
は、糖衣構文として<></>
とも使えます。記述量が遥かに少なくてすむので、とくに理由がなければ、こちらを使用しましょう。
import React from 'react'
function Heading({ children }) {
return <h1>{children}</h1>
}
function Index() {
const text = 'Next.js!'
// <React.Fragment></React.Fragment>は<></>とも書ける
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<p>divでラップしたないねん</p>
</>
)
}
export default Index
ファイルを分けてみましょう。
$ mkdir components
$ touch components/Heading.js
// components/Heading.js
// {} と return を省略できる
function Heading({ children }) {
return <h1>{children}</h1>
}
export default Heading
// pages/index.js
import React from 'react'
import Heading from '../components/Heading'
function Index() {
const text = 'Next.js!'
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<p>divでラップしたないねん</p>
</>
)
}
export default Index
map
メソッドで要素の反復処理をしてみましょう。
// pages/index.js
import React from 'react'
import Heading from '../components/Heading'
// 配列
const member = ['ネズミ', '牛', 'トラ', 'うさぎ']
function Index() {
const text = 'Next.js!'
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<ul>
{member.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
</>
)
}
export default Index
onClick
でイベント発火できます。
// pages/index.js
import React from 'react'
import Heading from '../components/Heading'
const member = ['ネズミ', '牛', 'トラ', 'うさぎ']
function Index() {
const text = 'Next.js!'
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<ul>
{member.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
{/* onClickに関数を書く */}
<button onClick={() => console.log('onClick')}>ボタン</button>
</>
)
}
export default Index
useStateで関数コンポーネントに状態をもたせる
React 16.8で、hooksという新機能が追加されました。Reactでstate
などの機能を使う場合、これまではクラスで書かないといけませんでしたが、hooksの登場で関数コンポーネントでも副作用のある機能を使うことができるようになりました。
今回は、関数コンポーネントに状態をもたせることができる、useState
を使ってみましょう。
// pages/index.js
// `useState`をインポート
import React, { useState } from 'react'
import Heading from '../components/Heading'
const member = ['ネズミ', '牛', 'トラ', 'うさぎ']
function Index() {
const text = 'Next.js!'
/**
* const [変数, 変数の値を変える関数] = useState(初期値)
* 以下では、`value`変数の初期値に`No, Click.`の文字列を代入しています。
* setValue('Yes, Click!!')を実行すると、
* valueの値を`No, Click.`から`Yes, Click!!`に変えることができます。
*/
const [value, setValue] = useState('No, Click.');
const onClickEvent = () => setValue('Yes, Click!!');
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<ul>
{member.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
<button onClick={() => console.log('onClick')}>ボタン</button>
{/* クリックすると、`No, Click.`が`Yes, Click!!`に変わる */}
<button onClick={onClickEvent}>{value}</button>
</>
)
}
export default Index
Next.js独自の機能
Link
コンポーネントでルーティングさせてみましょう。
// pages/index.js
import React, { useState } from 'react'
import Link from 'next/link' // Linkコンポーネントを追加
import Heading from '../components/Heading'
const member = ['ネズミ', '牛', 'トラ', 'うさぎ']
function Index() {
const text = 'Next.js!'
const [value, setValue] = useState('No, Click.');
const onClickEvent = () => setValue('Yes, Click!!');
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<ul>
{member.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
<button onClick={() => console.log('onClick')}>ボタン</button>
<button onClick={onClickEvent}>{value}</button>
{/* Linkコンポーネントでルーティングできる */}
<Link href="/batman"><a>バットマンページへ</a></Link>
</>
)
}
export default Index
pages/batman.js
を作成した上、バットマンページへのリンクをクリックすると、再読み込みなしでページ遷移できます。つまり、SPAです。
$ touch pages/batman.js
// pages/batman.js
import React from 'react'
function Batman() {
return <div>batman</div>
}
export default Batman
getInitialProps
で非同期データ取得
getInitialProps
は、Next.jsのライフサイクルメソッドです。ページが読み込まれたときはサーバーサイドで実行され、以降、Link
コンポーネントによって別のpages
コンポーネントへ移動した場合にクライアントサイドで実行されます。
以下の実装をして、http://localhost:3000
からhttp://localhost:3000/batman
に遷移したときと、http://localhost:3000/batman
をリロードしたときのコンソールの表示を確認してみましょう。
// pages/batman.js
import React from 'react'
function Batman({ text }) {
return <div>{text}</div>
}
Batman.getInitialProps = () => {
const text = 'I am Batman !!'
return { text } // returnしたオブジェクトをコンポーネントのPropsとして受け取れます
}
export default Batman
遷移したときはブラウザ側のコンソール、リロードしたときは開発側のコンソールに、それぞれログが出たかと思います。
Next.jsはサーバーサイドレンダリングのためのフレームワークなので、今書いているJavaScriptが**サーバーサイド(Node.js)なのか?**それとも、**クライアントサイドなのか?**を意識することが必要です。
非同期でデータ取得
バットマンAPIを叩いて、非同期に情報を取得してみましょう。ページ読み込み時になにかしらの処理をする場合は、getInitialProps
メソッドを使います。
Node.jsではfetchメソッドが使えないので、isomorphic-unfetch
をインストールして使います。
$ npm i isomorphic-unfetch
// pages/batman.js
import React from 'react'
import fetch from 'isomorphic-unfetch'
function Batman({ shows }) {
return (
<div>
<h1>Batman TV Shows</h1>
<ul>
{shows.map(show => (
<li key={show.id}>
<div><img src={show.image.medium} /></div>
<div>{show.name}</div>
</li>
))}
</ul>
</div>
)
}
Batman.getInitialProps = async () => {
const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
const data = await res.json();
return {
shows: data.map(entry => entry.show)
}
}
export default Batman
サーバーサイドレンダリングの使い所
たとえば、動的なコンテンツで
<title>
要素や<meta>
要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。
クソアプリを作ったので、これを実際に試してみましょう。
$ touch pages/nameApp.js
$ touch pages/yourName.js
// nameApp.js
import React, { useState } from 'react'
import { useRouter } from 'next/router'
function NameApp() {
const [name, setValue] = useState('')
/**
* Next.jsのルーターオブジェクト
* https://nextjs.org/docs#userouter
*/
const router = useRouter()
const onClickEvent = () => {
// yourname?name=【name】に遷移する
router.push({
pathname: '/yourName',
query: { name },
})
}
const onChangeEvent = event => setValue(event.target.value)
return (
<>
<div>君の名は。。。</div>
<input value={name} onChange={onChangeEvent} />
<button onClick={onClickEvent}>click!!</button>
</>
)
}
export default NameApp
nameApp.js
のやっていることは、ReactやNext.jsを使わない方法で書くとこんな感じです。
<form action="yourName/" method="GET">
<div>君の名は。。。</div>
<input name="name"/>
<button type="submit">click!!</button>
</form>
続いて、遷移先のyourName.js
を実装します。
// yourName
import React from 'react'
import Head from 'next/head'
function YourName({ query }) {
const { name } = query
return (
<>
{/* Headコンポーネントで`title`や`meta`が設定できる */}
<Head>
<title>{name} | YourName</title>
<meta name="description" content={`君の名は、${name}ですね。`} />
</Head>
<div>
君の名は、<strong>{name}</strong>ですね。
</div>
</>
)
}
YourName.getInitialProps = ({ query }) => {
return { query }
}
export default YourName
フォームに名前を入力して隣のボタンをクリックすると、入力した名前を表示することができる画期的なアプリです。
command + option + u
でソースを確認してみましょう。サーバーから取得したHTMLの段階で、title
やmeta
が設定されていることがわかります。
イメージを掴んでいただくために、試しにPHPで実装してみました。(PHPが全然わからないので細かいところはご勘弁を。。。(´;ω;`))
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>YourName</title>
</head>
<body>
<form action="yourName/" method="GET">
<div>君の名は。。。</div>
<input name="name"/>
<button type="submit">click!!</button>
</form>
</body>
</html>
上のHTMLでyourName/?name=ほげぼげ
みたいな感じになるので、PHPでパラメーターを受け取りHTMLとしてクライアントにレスポンスします。
<!-- yourName/index.php -->
<?php $name = htmlspecialchars($_GET['name']); ?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title><?php echo $name; ?> | YourName</title>
<meta name="description" content="君の名は、<?php echo $name; ?>ですね。" />
</head>
<body>
<div>君の名は、<?php echo $name; ?>ですね。</div>
</body>
</html>
より掘り下げたい場合は、公式ドキュメントを確認してください。
また、GitHubのexampleに豊富なサンプルがあるので、とても参考になります。
EmotionでCSS in JS
Emotionとは、JavaScriptでCSSスタイルを記述するために設計されたライブラリです。後発ライブラリのため、styled-component等の良いとこ取りをしています。
必要なパッケージをインストールしましょ。
- @emotion/styled
- @emotion/core
$ npm i @emotion/styled @emotion/core
Emotionを使ってみよう
@emotion/styledを使い、styled-componentライクなコンポーネントを作ってみます。
// components/Heading.js
// @emotion/styledをインポート
import styled from '@emotion/styled'
// styled.{要素}`{css}` の形で使用します。
// 定数に代入することで、コンポーネントとして利用できます。
// ブラウザ上ではユニークな文字列のCSSクラスが付与されるので、CSSはスコープになります。
const HeadingStyle = styled.h1`
font-size: 20px;
color: red;
`
function Heading({ children }) {
return <HeadingStyle>{children}</HeadingStyle>
}
export default Heading
CSS部分はテンプレートリテラルなので、${}
内でJavaScriptが利用できます。
// components/Heading.js
import styled from '@emotion/styled'
// フォントサイズを定数化
const fontSize = 20
// テンプレートリテラル内で定数を使用
const HeadingStyle = styled.h1`
font-size: ${fontSize}px;
color: red;
`
function Heading({ children }) {
return <HeadingStyle>{children}</HeadingStyle>
}
export default Heading
コンポーネント側からProps経由で値を渡すことができます。別ファイルにしてデータを渡してみましょう。
$ touch components/HeadingStyle.js
// components/HeadingStyle.js
import styled from '@emotion/styled'
// ES Modules
// 関数の引数としてデータを受け取ります
export const HeadingStyle = styled.h1`
font-size: ${props => props.fontSize}px;
color: red;
`
// components/Heading.js
import { HeadingStyle } from './HeadingStyle'
const fontSize = 20
function Heading({ children }) {
return <HeadingStyle fontSize={fontSize} >{children}</HeadingStyle>
}
export default Heading
SCSSのようにネストが使えます。
// components/HeadingStyle.js
import styled from '@emotion/styled'
// SCSSのように&が使えます。
export const HeadingStyle = styled.h1`
font-size: ${props => props.fontSize}px;
color: red;
&:hover {
color: green;
}
`
CSS in JSのメリット
たとえば、ブレークポイントをJavaScriptで管理すれば、Carouselのライブラリ等と共通の値を使うことができます。
$ mkdir const
$ touch const/breakPoints.js
// const/breakPoints.js
const breakPoints = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200
}
export default breakPoints
// components/HeadingStyle.js
import styled from '@emotion/styled'
import breakPoints from '../const/breakPoints'
export const HeadingStyle = styled.h1`
font-size: ${props => props.fontSize}px;
color: red;
@media (min-width: ${breakPoints.md}px) {
color: green;
}
`;
react-slickを使ってみましょ。
$ npm i react-slick slick-carousel raw-loader
$ touch next.config.js
next.config.js
で、Next.jsが隠蔽しているwebpackの設定にアクセスできます。raw-loaderを追加して、CSSファイルを扱えるようにします。
// next.config.js
module.exports = {
webpack: config => {
config.module.rules.push({
test: /\.css$/,
use: "raw-loader"
});
return config
}
}
スライダーコンポーネントを作り、トップページで使ってみましょう。
$ touch components/MySlider.js
// components/MySlider.js
import React from 'react'
import styled from '@emotion/styled'
import Slider from 'react-slick'
import slickCss from 'slick-carousel/slick/slick.css'
import slickThemeCss from 'slick-carousel/slick/slick-theme.css'
import breakPoints from '../const/breakPoints'
const settings = {
infinite: false,
slidesToShow: 2,
slidesToScroll: 2,
responsive: [
{
breakpoint: breakPoints.md, // const/breakPoints.jsの値が使える
settings: {
infinite: true,
slidesToShow: 1,
slidesToScroll: 1,
}
}
]
};
const SliderWrapperStyle = styled.div`
${slickCss}
${slickThemeCss}
`
function MySlider({ member }) {
return (
<SliderWrapperStyle>
<Slider {...settings}>
{member.map((animal, index) => (
<div key={index}>{animal}</div>
))}
</Slider>
</SliderWrapperStyle>
)
}
export default MySlider
// pages/index.js
import React, { useState } from 'react'
import Link from 'next/link'
import MySlider from "../components/MySlider";
import Heading from '../components/Heading'
const member = ['ネズミ', '牛', 'トラ', 'うさぎ']
function Index() {
const text = 'Next.js!'
const [value, setValue] = useState('No, Click.');
const onClickEvent = () => setValue('Yes, Click!!');
return (
<>
<Heading>
<span>{`Hello, ${text}`}</span>
</Heading>
<button onClick={() => console.log("onClick")}>ボタン</button>
<button onClick={onClickEvent}>{value}</button>
<Link href="/batman">
<a>バットマンページへ</a>
</Link>
{/* member配列をPropsで渡す */}
<MySlider member={member} />
</>
);
}
export default Index
他にもEmotionでいろいろなことができるので、ぜひ掘り下げてみてください。