MUIのBoxコンポーネントのCSS GridとGridコンポーネントのMUIグリッドの違い

Dashboardでは、

<Box gridColumn={`span ${gridSize}`}>

を使っていました。
これは CSS Grid を利用したレイアウトです。

一方、

<Grid item xs={12}>

は Material UIのGridコンポーネントを利用したレイアウトです。

どちらも「横幅を何分使うか」を指定する考え方ですが、

Grid item xs={12} は MUI Grid(12列グリッドシステム)
gridColumn="span 12" は CSS Grid

という違いがあります。 どちらもよく使われますが、仕組みは異なります。

JWT認証のRefreshToken (リフレッシュトークン)とは?

リフレッシュトークン(Refresh Token)

期限切れになったアクセストークンを再発行するための「更新用のトークン」です。

JWT認証では、この考え方がとても重要です。

なぜ必要なの?

例えば、ログインすると

ログイン
    │
    ▼
アクセストークン発行

このアクセストークンには有効期限があります。

例えば

有効期限:30分

30分経つと

401 Unauthorized

になってしまいます。

毎回ログインさせるのは不便

もしリフレッシュトークンがなければ、

30分経過

↓

期限切れ

↓

もう一度ID・パスワード入力

になります。

ユーザーからするとかなり面倒です。

リフレッシュトークンがある場合

ログイン時に

ログイン

↓

Access Token(30分)

Refresh Token(30日)

の2つが発行されます。

イメージは

ログイン
    │
    ├── Access Token
    │      ↑ API呼び出しに使う
    │
    └── Refresh Token
           ↑ 更新専用

期限が切れたら

30分後

Access Token

↓

期限切れ

でも

Refresh Token

↓

新しいAccess Token発行

できます。

つまり

Access Token①
        │
        ▼
期限切れ
        │
Refresh Token
        │
        ▼
Access Token②

になります。

ユーザーは再ログイン不要です。

イメージ

ログイン
     │
     ▼
┌──────────────────────┐
│ Access Token         │
│ APIを呼ぶための鍵      │
│ 有効期限30分          │
└──────────────────────┘

┌──────────────────────┐
│ Refresh Token        │
│ 鍵を作り直すための鍵    │
│ 有効期限30日          │
└──────────────────────┘

検索APIの構成

今の流れ

  • Controller
$service->handle($request)

  • PaginationService
$request->condition()
$request->sort()
$request->direction()
$request->limit()

  • Repository
paginateByCondition(...)

  • queryByCondition()
$condition->q()

で検索。

  • fb593cc (HEAD -> main, origin/main) add: データベース操作まわりのファイル追加
  • ef4b7ac add: Domain/IndustryRepository, Infrastructure/IndustryRepository, Industry Model の追加
  • b243359 add: Industry/SearchRequest, IndustrySearchDto, IndustrySearchConditionを追加
  • ceaffc2 add: IndustryControllerの追加

LaravelのDIとは

LaravelのDIとは、
「Controllerの引数に書くだけで、必要なクラスを自動生成して渡してくれる仕組み」

■ ■ Laravel内部で起きていること

リクエストが来ると👇

  1. FaqControllerが必要
  2. index()を呼ぶ
  3. 引数を見て必要なクラスを調べる
  4. PaginationServiceが必要と判断
  5. new PaginationService() する
  6. メソッドに渡す ■ ■ つまり 「自動で new して、引数に入れてくれる」 ■ ■ なんでできるのか

Laravelには「コンテナ」があるからです👇

Service Container(DIコンテナ)

JavaScriptでいうtruthy / falsyとは?

!!project.isFavorite

を理解するには:

  • truthy
  • falsy
  • !
  • !!

を順番に理解するとわかります。

■ まず truthy / falsy とは?

JavaScriptでは:

「true / false ではない値」も、条件式で true/false 扱いされる

という特徴があります。

■ truthy

条件式で:true扱いされる値

■ 例

'hello'
123
[]
{}
true

■ if に入れると

if ('hello') {
  console.log('実行')
}

実行される

■ falsy

条件式で:false扱いされる値

■ JavaScriptの falsy 一覧(超重要)

false
0
''
null
undefined
NaN

■ 例

if (0) {
  console.log('実行')
}

実行されない

■ ! の意味

否定(NOT)

■ 例

!true

false

■ 次に「!!」

ここが本題。

■ !! の意味

Boolean化

■ やってること

!!value

=

!(!value)

■ 例

!!'hello'

■ 手順 ①

!'hello'

false

!false

true

■ 結果

!!'hello' === true

■ なぜ !! つける人いる?

「確実に boolean にしたい」

■ よくある例

const isLoggedIn = !!user

■ user がnullなら

false

■ user object なら

true

useStateフックがわからなくなったら

「お気に入り状態を React の state として管理している」

const [favorite, setFavorite] = useState<boolean>(project.isFavorite)
  • favorite → 現在のお気に入り状態
  • setFavorite → お気に入り状態を変更する関数

state とは?

簡単に言うと:

画面用の変数

普通の変数との違い

  • 普通の変数
let favorite = true

変更しても:

画面更新されない
  • useState
const [favorite, setFavorite] = useState(true)

変更すると:

React再レンダリング

useAuthの責務の分離(肥大化問題)

問題となっているuseAuthフック

結論

useAuthは「認証ドメイン全部」を抱えすぎていて、本来は分割すべき状態

import { useEffect } from 'react'
import axios from 'axios'
import { useRouter } from 'next/router'
import { setCookie, destroyCookie, parseCookies } from 'nookies'
import useRequest, { fetcher } from './useRequest'
import type { AxiosError, AxiosResponse } from 'axios'
import { AUTH_TOKEN_KEY } from '@/constant'
import { APIRequest, HTTPMethod } from '@/lib/api/APIRequest'
import { User, UserAndToken, UserDetail, UserExperience, UserPreference } from '@/models'

interface Referral {
  redirect?: string
}

export default function useAuth({ redirectTo = '', redirectIfFound = false } = {}) {
  const {
    data: user,
    mutate,
    error,
  } = useRequest<User>({ url: '/v1/me' } as APIRequest, {
    onError: (error: any) => {
      if (error.response?.status != 401) return
      handleAuthError()
    },
  })
  const router = useRouter()
  const loadingUser = user === undefined && !error

  useEffect(() => {
    if (loadingUser) return
    if ((redirectTo && !redirectIfFound && !user) || (redirectIfFound && user)) {
      router.push(redirectTo)
    }
  }, [user, redirectIfFound, redirectTo, loadingUser, router])

  const mutateUserDetail = (detail: UserDetail): Promise<User> => mutate({ ...user, detail })
  const mutateUserPreference = (preference: UserPreference): Promise<User> =>
    mutate({ ...user, preference })
  const mutateUserExperience = (experience: UserExperience): Promise<User> =>
    mutate({ ...user, experience })

  const setToken = (token: string) =>
    setCookie(null, AUTH_TOKEN_KEY, token, {
      maxAge: 30 * 24 * 60 * 60,
      path: '/',
    })

  const login = async (email, password, rememberMe = false): Promise<User> => {
    const response = await fetcher<UserAndToken>({
      url: '/v1/login',
      method: HTTPMethod.POST,
      params: { email, password },
    } as APIRequest)
    setToken(response.token)
    localStorage.setItem('rememberMe', rememberMe ? '1' : '0')

    mutate(response.user, { rollbackOnError: true })
    return response.user
  }

  const refresh = async (): Promise<void> => {
    try {
      const response = await fetcher<UserAndToken>({
        url: `/v1/refresh`,
        method: HTTPMethod.POST,
      })
      setToken(response.token)

      const user: User = await fetcher<User>({
        url: '/v1/me',
        method: HTTPMethod.GET,
      })
      mutate(user, { rollbackOnError: true })
    } catch (e) {
      console.error(e)
      logout(false)
    }
  }

  const register = async (email, password, passwordConfirmation): Promise<User> => {
    const registerVia = router.asPath
    const response = await fetcher<UserAndToken>({
      url: '/v1/register',
      method: HTTPMethod.POST,
      params: { email, password, passwordConfirmation, registerVia },
    } as APIRequest)

    setToken(response.token)

    mutate(response.user, { rollbackOnError: true })
    return response.user
  }

  const logout = async (needRequest = true) => {
    if (location.href.includes('login')) return
    if (location.href.includes('register')) return
    if (location.href.includes('apply/step1')) return
    if (location.href.includes('landing')) return

    const cookies = parseCookies()
    if (!cookies[AUTH_TOKEN_KEY]) return

    needRequest &&
      (await fetcher<User>({
        url: '/v1/logout',
        method: HTTPMethod.DELETE,
      } as APIRequest))
    destroyCookie(null, AUTH_TOKEN_KEY)
    mutate(null)
    localStorage.removeItem('rememberMe')
  }

  const leave = async () => {
    await fetcher({
      url: '/v1/user/leave',
      method: HTTPMethod.DELETE,
    } as APIRequest)
    destroyCookie(null, AUTH_TOKEN_KEY)
    mutate(null)
    localStorage.removeItem('rememberMe')
  }

  const forgotPassword = async (email): Promise<void> => {
    const response = await fetcher<User>({
      url: '/v1/forgot-password',
      method: HTTPMethod.POST,
      params: { email },
    } as APIRequest)
  }

  const resetPassword = async (email, password, passwordConfirmation): Promise<void> => {
    const token = router.query.token

    const response = await fetcher<UserAndToken>({
      url: `/v1/reset-password/${token}`,
      method: HTTPMethod.POST,
      params: { token, email, password, passwordConfirmation },
    } as APIRequest)
  }

  const setReferral = async (referral: string): Promise<string | null> => {
    if (!!user) {
      return null
    }

    const respnse = await fetcher<Referral>({
      url: `/v1/set-referral/${referral}`,
      method: HTTPMethod.POST,
    } as APIRequest)

    return respnse.redirect ?? '/'
  }

  const handleAuthError = async () => {
    const rememberMe = JSON.parse(localStorage.getItem('rememberMe') ?? '0')
    !!rememberMe && (await refresh())
    !rememberMe && (await logout(false))
  }

  const onResponse = (response: AxiosResponse): AxiosResponse => response
  const onResponseError = (error: AxiosError): Promise<AxiosError> | undefined => {
    if (error.response?.status == 401 && error.config.method !== 'get') {
      handleAuthError()
      return
    }
    return Promise.reject(error)
  }
  axios.interceptors.response.use(onResponse, onResponseError)

  return {
    loadingUser,
    user,
    mutateUserDetail,
    mutateUserPreference,
    mutateUserExperience,
    login,
    register,
    logout,
    leave,
    forgotPassword,
    resetPassword,
    setReferral,
  }
}