YSNHatenaBlog

主にアプリやWebサービス開発について

Next.jsのバージョンを上げた

Next.jsのバージョンを上げている - YSNHatenaBlog

前回記事でNext.jsのバージョンを上げてハマっていうたけど、何も問題なかった。

Server Error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
pages/_document.tsx (71:4) @ Object.ctx.renderPage

  69 | 
  70 |   ctx.renderPage = () =>
> 71 |     originalRenderPage({
     |    ^
  72 |       enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
  73 |     })
  74 | 
Call Stack
Function.Document.getInitialProps
pages/_document.tsx (75:29)
Show collapsed frames

このissueによると、

github.com

package.json更新したりすると発生することがあるらしい。

一旦次のファイルを消して、

  • node_modules
  • dist
  • yarn.lock

yarnし直したら直った。

とりあえずNext.jsとnodeのバージョンは上がった。

Next.jsのバージョンを上げている

よいデータ設計が思いつかないので、気分転換も兼ねてNext.jsのバージョンをv11.1.0に上げようとしている。

% yarn
yarn install v1.16.0
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
[3/5] 🚚  Fetching packages...
error next@11.1.0: The engine "node" is incompatible with this module. Expected version ">=12.0.0". Got "10.16.0"
error Found incompatible module.
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

Nodeバージョンを上げる。 https://cloud.google.com/functions/docs/concepts/exec?hl=ja#runtimes https://cloud.google.com/functions/docs/concepts/nodejs-runtime?hl=ja runtimeをnodejs12や14にしたときの具体的なバージョンがわからない。

久々に触ったらndenv使ってたのでnodenvにする。

% anyenv uninstall ndenv
% anyenv install nodenv

(.anyenv/envs/ndenv にゴミが残ったんだけどなんでだ?)

せっかくなのでNode14にしても大丈夫そうか見る。 https://nodejs.org/ja/blog/uncategorized/10-lts-to-12-lts/ https://nodejs.medium.com/node-js-version-14-available-now-8170d384567e

あまり影響無さそうなので上げてみる。

細かいバージョンがわからないので、とりあえず14系の最後のバージョンに。

% nodenv install 14.17.5

yarn dev で次のエラー

Server Error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

むむむ...。しかもエラー箇所が良くわからない。

nodeとNextどっちのバージョンの問題か? Nextのバージョンだけ戻した。

Server Error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
pages/_document.tsx (71:4) @ Object.ctx.renderPage

  69 | 
  70 |   ctx.renderPage = () =>
> 71 |     originalRenderPage({
     |    ^
  72 |       enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
  73 |     })
  74 | 
Call Stack
Function.Document.getInitialProps
pages/_document.tsx (75:29)
Show collapsed frames

今度はちゃんと問題箇所が出てきた。

_document.tsx はあまり触ってないんだけど、ここをFunctional Componentで書かないと駄目なのかな?

自作サービスの方針転換

今練習で作ってるテニスコート検索のサイト。各テニスコート詳細ページのタイトルを「(コート名)の場所」にしたけど、SEO的に悪化したような気がするので戻した。

tennico.app

とはいえそもそも流入ほぼ無い状態。検索キーワード候補を見ても、「テニスコート 安い」みたいのが出てくるので、やはり場所より料金検索ニーズの方が高そうだなぁと思い、料金で検索できるようにしたいと考え中。

以前書いた気もするが、テニスコートの料金体系って結構複雑。次の要因で料金が変わる。

  • 時間帯 - 早朝は割引があったりする。
  • 季節 - 冬場は午後のコートを使える時間が短かかったり、ナイターを利用する時間が早まったりする。
  • コート - 同じ場所にあるコートでも、センターコートは高かったり、古いクレーやハードコートは安かったりする。
  • 地域住民かどうか - 地域の住民は割引があったりする。
  • 照明利用有りか - ナイターを利用すると基本的に追加料金を取られる。

あとは、1時間単位で借りられるところもあれば2時間単位、半日単位で借りられたりするところもあるんだけど、どうやって検索フィルタ作ればいいの?とか。1時間あたりの料金??

iOSのフォーム選択で画面が拡大されてしまうのを防ぐ

また細かい話。 ここ参考にさせていただいただけなのですが。formのフォントが16px未満のときに発生するらしい。 これだと入力後にもう一度全画面表示に直さないと検索結果が見られないので不便。

Before

f:id:yosuke403:20210608051148g:plain

pgmemo.tokyo

16pxに設定して修正。

After

f:id:yosuke403:20210608051153g:plain

viewportの設定でもできるけど効かないという話も。

cly7796.net

Safariのoverflow-wrap: anywhere

Safariで開くとoverflow-wrap: anywhere の範囲の文字列が折り返されず、画面外にテキストが続いてしまうことがある件。

そもそもoverflow-wrapはSafariで非対応。 developer.mozilla.org

これで折り返せた。

overflow-wrap: 'anywhere'
word-break: 'break-all'

Redux Toolkitがすごい便利だった

画面遷移しても状態保存しておこうと思い、再度reduxを見直していたが、これを見つけてちょっと感動した。

redux-toolkit.js.org

reduxの構成にしたい人はこれ入れておくとよさそう。

action, reducer, selectorの記述が少ない

reduxは何と言ってもコードの記述量が増えるのがめんどくさいのだが、とてもスマートに書ける。

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { CourtDoc } from 'models/court'
import type { RootState } from 'store'

type Mode = 'text' | 'location'
type Geo = {
  lat: number
  lng: number
}

interface HomeState {
  mode: Mode
  zoom: number
  center: Geo
  courts?: CourtDoc[]
}

const initialState: HomeState = {
  mode: 'text',
  zoom: 11,
  center: { lat: 35.681236, lng: 139.767125 },
}

export const homeSlice = createSlice({
  name: 'home',
  initialState,
  reducers: {
    changeMode: (state, action: PayloadAction<Mode>) => {
      state.mode = action.payload
    },
    changeZoom: (state, action: PayloadAction<number>) => {
      state.zoom = action.payload
    },
    changeCenter: (state, action: PayloadAction<Geo>) => {
      state.center = action.payload
    },
  },
})

export const { changeMode, changeZoom, changeCenter } = homeSlice.actions

export const selectMode = (state: RootState): Mode => state.home.mode
export const selectZoom = (state: RootState): number => state.home.zoom
export const selectCenter = (state: RootState): Geo => state.home.center
export const selectCourtDocs = (state: RootState): CourtDoc[] | undefined =>
  state.home.courts

export default homeSlice.reducer

actionのstringを自分で書かなくても、 createSlicenameと関数名から自動でいい感じに生成してくれる。 reducers の書き換えをしてもglobalに影響が出ないようになっている。

reselect

まだやってないけど createSelector でSelectorを作ると reselect が使われてパフォーマンス上がる模様。 createSelector | Redux Toolkit

TypeScript対応

型がちゃんと付く。

フラグ一個でredux-devtools-extensionがつく

export const store = configureStore({
  reducer: {
    firebase: firebaseReducer,
    firestore: firestoreReducer,
    home: HomeReducer,
  },
  devTools: true, // <- これだけ
})

副作用の対応

前述のcreateSliceのところで、

import { createAsyncThunk, ... } from '@reduxjs/toolkit'
import { search } from 'models/search'
...

export const searchByText = createAsyncThunk(
  'home/searchByText',
  async (params: { text: string; hits: number }) => {
    const courtDocs = await search(params.text, params.hits)
    return courtDocs
  }
)

export const homeSlice = createSlice({
  ...,
  extraReducers: {
    [searchByText.fulfilled.type]: (
      state,
      action: PayloadAction<CourtDoc[]>
    ) => {
      state.courts = action.payload
    },
  },
})

...
export default homeSlice.reducer

これでいい。

すごい便利だ...。まだかじっただけなので、もうちょっと調べる。

google-map-reactから@react-google-maps/apiに乗り換え

もともと google-map-react を使っていたのを @react-google-maps/api に変更。

google-map-react はダウンロード数が多く、当初必要だった要件を満たしそうだったので導入したものの、デフォルトのマーカーへの変更を考えたときに、

new maps.Marker({
  position: { lat, lng }
  map,
})

のようにライブラリを直接呼び出す必要があり、Reactっぽくなく使いにくかった。 クリックイベントも取得しにくいので、他ライブラリを調査することに。

google-maps-react というそっくりな名前のパッケージもあったが、あまりメンテされてなさそうなので不採用。

結局こちらを使うことに。

www.npmjs.com

<GoogleMap
mapContainerStyle={{
  width: '100%',
  height: '100%',
}}
onIdle={onMapMoved}
onLoad={onLoaded}
>
{courtDocs.map((courtDoc) => {
  return (
    <Marker
      key={id}
      position={{ lat, lng }}
      onClick={() => onClickMarker(id)}
    />
  )
})}
</GoogleMap>

これなら Marker をReact Componentとして扱える。

吹き出しInfoWindowコンポーネントを使う。 これも Marker と同じように GoogleMap の子として追加すればよい。 吹き出しは✗で消せるので、消えたときに InfoWindow も取り除かないと再表示がうまくいかなかった。