2022/09/02
Next.js + TailwindCSS + Reduxのプロジェクト立ち上げ時のメモ
Next.js + Tailwind CSS + Redux のプロジェクト作成時の備忘録です。
前回のものを少し改良しています。
(前回はRecoilを使ったバージョンですが・・結構凝ったアプリはReduxの方が制御しやすかったので、Reduxを使ったバージョンです。)
末尾に、(例えばフォームsubmit時などに便利な)ページ全体をローディング中にするコンポーネントをreduxで作ったものを掲載しています。
環境
% node -v
v18.8.0
% yarn -v
1.22.19
% npx -v
8.18.0
プロジェクトの作成・Tailwind CSSの初期化
# create app
% npx create-next-app@latest --ts your-project-name
% cd your-project-name
# add .idea to .gitignore(jetbrain社製のIDE"webstorm"を使う方用)
% cat <<EOF >> .gitignore
# ide
.idea/
EOF
# src directory
% cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
EOF
% mkdir src && \
mv pages src && \
mv styles src
# edit next.config.js
% cat <<'EOF' > next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
newNextLinkBehavior: true,
scrollRestoration: true,
images: {
allowFutureImage: true,
},
},
}
module.exports = nextConfig
EOF
# introduce tailwind and some utilities
% yarn add --dev \
tailwindcss \
@tailwindcss/forms \
@tailwindcss/typography \
postcss \
autoprefixer \
sass \
styled-components \
@types/styled-components
% yarn add \
@headlessui/react \
@heroicons/react \
clsx \
focus-visible \
postcss-focus-visible \
use-debounce \
framer-motion
% npx tailwindcss init -p
tailwind.config.jsを編集
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: ({ colors }) => ({
primary: colors.rose,
}),
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
}
global.cssを編集
@import url('https://rsms.me/inter/inter.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
prettierを導入
% yarn add --dev \
prettier \
eslint-config-prettier \
eslint-plugin-unused-imports \
prettier-plugin-tailwindcss
.eslintrc.jsonを以下のように編集
{
"extends": ["next/core-web-vitals", "prettier"],
"plugins": ["unused-imports"],
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "off"
}
}
.prettierignore ファイルを追加
cat <<'EOF' > .prettierignore
###
# Place your Prettier ignore content here
###
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
# static resources
public
EOF
prettierの設定ファイルを追加
cat <<'EOF' > prettier.config.js
module.exports = {
singleQuote: true,
semi: false,
tabWidth: 2,
useTabs: false,
plugins: [require('prettier-plugin-tailwindcss')],
}
EOF
package.jsonファイルにprettierのコマンド追加
{
"scripts": {
+ "lintfix": "prettier --write --list-different .",
}
}
# 以下のコマンドを実行して、prettierを実行してコードの整形が可能になりました。
% yarn lintfix
husky/commitlintを導入
% yarn add --dev \
husky \
lint-staged \
@commitlint/{cli,config-conventional}
% husky install
% cat <<'EOF' > .husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
yarn commitlint --edit $1
EOF
% cat <<'EOF' > .husky/common.sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi
EOF
% cat <<'EOF' > .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
yarn lint-staged
EOF
% cat <<'EOF' > commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
}
EOF
% chmod +x .husky/commit-msg
% chmod +x .husky/common.sh
% chmod +x .husky/pre-commit
package.jsonにlint-staged
を追加
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint"
],
"*.**": [
"prettier --check --ignore-unknown"
]
},
Reduxの導入
% yarn add \
@reduxjs/toolkit \
react-redux \
next-redux-wrapper
% mkdir -p src/store/app
% cat <<'EOF' > src/store/index.ts
import { Action, ThunkAction, configureStore } from '@reduxjs/toolkit'
import { appLoadingSlice } from '@/store/app/loading'
import { createWrapper } from 'next-redux-wrapper'
const makeStore = () =>
configureStore({
reducer: {
[appLoadingSlice.name]: appLoadingSlice.reducer,
},
devTools: true,
})
export type AppStore = ReturnType<typeof makeStore>
export type AppState = ReturnType<AppStore['getState']>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
Action
>
export const wrapper = createWrapper<AppStore>(makeStore)
EOF
% cat <<'EOF' > src/store/app/loading.ts
import { createSlice } from '@reduxjs/toolkit'
import { AppState } from '@/store'
import { HYDRATE } from 'next-redux-wrapper'
export const appLoadingSlice = createSlice({
name: 'appLoading',
initialState: {
loading: false,
} as any,
reducers: {
setAppLoading(state, action) {
state.loading = action.payload
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
console.log('HYDRATE', state, action.payload)
return {
...state,
...action.payload.appLoading,
}
},
},
})
export const { setAppLoading } = appLoadingSlice.actions
export const selectAppLoading = (state: AppState) => state.appLoading.loading
export default appLoadingSlice.reducer
EOF
% cat <<'EOF' > src/pages/_app.tsx
import '@/styles/globals.css'
import { wrapper } from '@/store'
import type { AppProps } from 'next/app'
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default wrapper.withRedux(App)
EOF
% mkdir -p src/components/loadings
% cat <<'EOF' > src/components/loadings/AppLoading.tsx
import styled, { keyframes } from 'styled-components'
import { selectAppLoading } from '@/store/app/loading'
import { useSelector } from 'react-redux'
const AppLoading = () => {
const isAppLoading = useSelector(selectAppLoading)
if (isAppLoading) {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-25">
<Spinner className="loader mb-4 h-24 w-24 rounded-full border-4 border-t-4 border-gray-200 ease-linear"></Spinner>
</div>
)
} else {
return null
}
}
export default AppLoading
const spinAnimation = keyframes`
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
`
const Spinner = styled.div`
border-top-color: #f97316;
-webkit-animation: ${spinAnimation} 1.5s linear infinite;
animation: ${spinAnimation} 1.5s linear infinite;
`
EOF
% cat <<'EOF' > src/pages/index.tsx
import type { NextPage } from 'next'
import AppLoading from '@/components/loadings/AppLoading'
import { setAppLoading } from '@/store/app/loading'
import { useDispatch } from 'react-redux'
const Home: NextPage = () => {
const dispatch = useDispatch()
const turnLoadingOn = async () => {
dispatch(setAppLoading(true))
try {
await new Promise((resolve) => setTimeout(resolve, 1000))
} finally {
dispatch(setAppLoading(false))
}
}
return (
<>
<div className="bg-white font-inter tracking-tight text-gray-900 antialiased">
<main className="relative flex h-screen overflow-hidden bg-gray-50">
<div className="flex h-full w-full items-center justify-center">
<button
className="inline-flex justify-center rounded-lg border border-primary-300 py-[calc(theme(spacing.2)-1px)] px-[calc(theme(spacing.3)-1px)] text-sm text-primary-700 outline-2 outline-offset-2 transition-colors hover:border-primary-400 active:bg-primary-100 active:text-primary-700/80"
onClick={turnLoadingOn}
>
<span>App Loading</span>
</button>
</div>
</main>
</div>
<AppLoading></AppLoading>
</>
)
}
export default Home
EOF
References
関連する記事
Nuxt2からNuxt3への移行とNextJSとNuxt3の比較について
弊社ホームページとブログサイトをNuxt2からNuxt3ベースに移行しました。
[NextJs]Google Mapでマーカーをセンターに表示するコンポーネントの作成
NextJsアプリ内で、Google Mapを表示して、中心にマーカーを配置するコンポーネントを作成しました。
Next.js + TailwindCSS + Reduxのプロジェクト立ち上げ時のメモ
Next.js + Tailwind CSS + Reduxのプロジェクト作成時の操作ログです。
Next.js + TailwindCSSのプロジェクト立ち上げ時のメモ
Next.js + Tailwind CSSのプロジェクト作成時の操作ログです。