Next.jsでIonic Frameworkを使う
この記事は、Next.js Advent Calendar 2020の8日目です。 Ionic Framework(@ionic/react)をNext.jsで使おうとすると、だいたいこういうエラーに遭遇します。 なにが […]
目次
この記事は、Next.js Advent Calendar 2020の8日目です。
Ionic Framework(@ionic/react
)をNext.jsで使おうとすると、だいたいこういうエラーに遭遇します。
% yarn build
yarn run v1.22.5
$ yarn --cwd packages/web-app build
$ next build
info - Creating an optimized production build
info - Compiled successfully
> Build error occurred
ReferenceError: HTMLElement is not defined
なにがおきているのか
IonicはWebComponent(Stencil)で作られたフレームワークです。そのため、各タグを利用するためには、defineCustomElements()
を実行してカスタムエレメントを登録する必要があります。
そして@ionic/react
では、この事前準備系はimportするだけで内部的に実行されるように作られています。便利ですね。
しかしこの内部的によしなにする部分がNext.jsやGatsbyで引っかかります。というのも、defineCustomElements
の引数はwindow
だからです。これはNode.jsでは存在しないものですので、SSRまたはSSGを実行すると、Not Foundエラーが発生します。
コードを見る限り、typeof window !== 'undefined'
のチェックはあるのですが、どうも動かないのでどうしたものかなというところです。Issueなどを調べるといろいろと各自試行錯誤されている様子が窺えます。
個人的解決方法: 思い切って@ionic/core
をつかう
現在の本サイトがNext.js + Ionicなのですが、実はここでは@ionic/react
を使っていません。
Next.jsのサンプルリポジトリを見ると、Stencilを使ったversionが用意されていました。
https://github.com/vercel/next.js/tree/canary/examples/with-stencil
これをみるに、Web Componentが動かないのではなく、defineCustomElement
の実行場所次第でこけることがあるという理解が良さそうです。
ということで、Ionicの本体でもある@ionic/core
そのものを使う形で実装します。
Step by step
Step1: Create Next.js application
# Setup project
$ npx create-next-app nextjs-ionic
# Go to web root
$ cd nextjs-ionic
# Put tsconfig.json
$ touch tsconfig.json
# Install TypeScript packages
$ yarn add -D typescript @types/react @types/node
Step2: Install Ionic components
@ionic/core
を使う場合、一緒にionicons
もインストールしましょう。
$ yarn add @ionic/core ionicons
Step3: Define Custom Elements Types
JSXでIonicのタグを使う場合、JSXの型情報に追加登録してやる必要があります。
pages/_app.tsx
import { JSX as LocalJSX} from '@ionic/core'
import {JSX as IoniconsJSX} from 'ionicons'
import { HTMLAttributes, ReactText } from 'react'
type ToReact<T> = {
[P in keyof T]?: T[P] & Omit<HTMLAttributes<Element>, 'className'> & {
class?: string;
key?: ReactText;
}
}
declare global {
export namespace JSX {
interface IntrinsicElements extends ToReact<LocalJSX.IntrinsicElements & IoniconsJSX.IntrinsicElements> {}
}
}
Step4: Define Custom Element and import CSS
続いでCSSとloaderを実装します。defineCustomElements
を実行する必要がありますので、こちらはuseEffect
を使って副作用側で処理しましょう。
pages/_app.tsx
import React, { useEffect } from 'react'
import { defineCustomElements as ionDefineCustomElements } from '@ionic/core/loader';
/* Core CSS required for Ionic components to work properly */
import '@ionic/core/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/core/css/normalize.css';
import '@ionic/core/css/structure.css';
import '@ionic/core/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/core/css/padding.css';
import '@ionic/core/css/float-elements.css';
import '@ionic/core/css/text-alignment.css';
import '@ionic/core/css/text-transformation.css';
import '@ionic/core/css/flex-utils.css';
import '@ionic/core/css/display.css';
function MyApp({ Component, pageProps }) {
useEffect(() => {
ionDefineCustomElements(window)
})
return <p>Hello</p>
}
export default MyApp
Finally: Use Ionic component into the application
あとはIonic Angularライクにコードを書くだけです。
pages/home.tsx
import Image from 'next/image'
export default function Home() {
return (
<ion-refresher slot="fixed" closeDuration="10ms">
<ion-refresher-content />
<ion-card>
<ion-card-header>
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
<ion-card-title>Card Title</ion-card-title>
</ion-card-header>
<ion-card-content>
<Image
src="./images"
alt="Picture of the author"
width={500}
height={300}
/>
Keep close to Nature's heart... and break clear away, once in awhile,
and climb a mountain or spend a week in the woods. Wash your spirit clean.
</ion-card-content>
</ion-card>
</ion-refresher>
)
}
ioniconのSVGが表示されない場合は、webpack + リダイレクトで対応する
再現性がある問題なのかまで掘り下げれていないのですが、ioniconのアイコンSVGが描画されない時がありました。
SVGのパスが意図した通りにならない様子なので、自分のサイトの場合は、WebpackのCopyPluginを使ってコピーして、Netlifyのリダイレクトを使って調整しています。
const CopyPlugin = require('copy-webpack-plugin')
const path= require('path')
module.exports = {
...
pageExtensions: ['ts', 'tsx'],
webpack: (config) => {
config.plugins.push(
new CopyPlugin({
patterns: [{
from: path.join(__dirname, 'node_modules/ionicons/dist/ionicons/svg'),
to: path.join(__dirname, 'public/svg')
}]
})
)
return config
}
}
Netlify側はこんな感じ
[[redirects]]
from = "/:lang/:slug/svg/:filename"
to = "/svg/:filename"
status = 200
[[redirects]]
from = "/:slug/svg/:filename"
to = "/svg/:filename"
status = 200
注意点など
割と力技的解決ですが、簡単なブログサイトを作る程度であれば問題なく動いています。いまのところ。
ただ、ionXXX
系の挙動がちょっと怪しい気配があったり、@ionic/react
で未対応のものはやっぱり動かなかったりと、なかなか縛りプレイになることはご留意ください。
サンプル
- GitHub: https://github.com/hideokamoto/example-nextjs-with-ionic
- Application: ionic-with-nextjs.netlify.app