10日で作るWebサイト(day6)

HTML/CSS

連続10日間でサイト作る予定だったのですが、別件で忙しかったため半月程度時間が空いてしまった。しかたがないので合計10日間での開発日記とする!

day6の課題は「解答フォームの作成」です。UIフレームワークはMUIを利用しているため、FormControlなどを使って実装しようかとも考えたのですが、4択選択肢だけの実装となるため愚直に実装した方がシンプルです。

コンポーネントの実装はこんな感じ。


export const Quiz = ({ quiz }: { quiz: Quiz }) => {
  const [answer, setAnswer] = useState<number | null>(null)
  const { showSnackbar } = useSnackbar()

  const handleAnswer = () => {
    if (answer == null) {
      showSnackbar('warning', 'please select answer.')
      return
    }

    // todo
  }

  return (
    <Box
      width="80%"
      mx="auto"
      my={4}
      p={4}
      display="flex"
      sx={{
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'space-around',
        borderRadius: '10px',
      }}
      className="SignInDialog"
    >
      <AppIcon sx={{ fontSize: 80 }} />
      <Typography my={3} variant="h6">
        第N問
      </Typography>
      <Typography my={3} variant="h6">
        {quiz.question}
      </Typography>
      <Box flexDirection="column" sx={{ display: 'flex', gap: 2 }}>
        {quiz.choices.map((choice, index) => (
          <Choice
            key={index}
            choice={choice}
            index={index}
            isSelected={answer === index}
            onClick={() => setAnswer(index)}
          />
        ))}
      </Box>
      <Box width="50%" display="flex" mt={3} sx={{ justifyContent: 'space-around' }}>
        <Button variant="outlined" onClick={handleAnswer}>
          ANSWER
        </Button>
      </Box>
    </Box>
  )
}

type ChoiceProps = {
  choice: string
  index: number
  isSelected: boolean
} & BoxProps

const Choice = ({ choice, index, isSelected, ...props }: ChoiceProps) => {
  return (
    <Box sx={{ display: 'flex', alignItems: 'start' }} {...props}>
      <Typography width={16} flexShrink={0}>
        {index + 1}
      </Typography>
      <Typography color={isSelected ? 'primary' : undefined}>{choice}</Typography>
    </Box>
  )
}

className=”SignInDialog”
って感じでcssに定義したクラスセレクタを利用しているんですが、cssファイル自体使わない方がベターなのでしょうか?sxやstyle propsにゴリゴリにスタイルを書いていくのがどうも好みじゃない。複雑なデザインを実現しようとすると長文になってコンポーネントの視認性が悪くなるのが特にイヤ。ファイル分離したいような、かといってファイルが別だと参照するのがめんどいような。だれか上手く解決してくれないかなぁ。

Snackbarのcontext化

MUIのSnackbarは使いやすいのですが、setIsOpen的なものを都度書くのは辛い。
私の場合は複数個のSnackbarを同時に表示することはないため、Globalに1つSnackbarを定義し、ContextProviderでSnackbarの表示ロジックを使いまわすことにします。

Snackbar Component

内部にAlertコンポーネントを利用し、severityとmessageを渡すと表示される作りにします。

import { Alert, AlertColor, Snackbar as MuiSnackbar } from '@mui/material'

type Props = {
  isOpen: boolean
  severity: AlertColor
  message: string
  onClose: () => void
}
export function Snackbar({ isOpen, severity, message, onClose }: Props) {
  return (
    <MuiSnackbar
      open={isOpen}
      autoHideDuration={3000}
      onClose={onClose}
      anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
    >
      <Alert variant="filled" onClose={onClose} severity={severity}>
        {message}
      </Alert>
    </MuiSnackbar>
  )
}

Snackbar Context Provider

createContextしてContextProviderにshowSnackbarという関数を持たせます。
この関数を簡単に呼び出せるようにuseSnackbarというhookも定義しておきます。

import { AlertColor } from '@mui/material'
import { createContext, useContext, useState } from 'react'
import { Snackbar } from '../components/Snackbar'

type SnackbarContext = {
  showSnackbar: (severity: AlertColor, message: string) => void
}
const SnackbarContext = createContext<SnackbarContext>({} as SnackbarContext)

export const SnackbarProvider = ({ children }: { children: React.ReactNode }) => {
  const [isOpen, setIsOpen] = useState(false)
  const [severity, setSeverity] = useState<AlertColor>('info')
  const [message, setMessage] = useState('')

  const showSnackbar = (severity: AlertColor, message: string) => {
    setIsOpen(true)
    setSeverity(severity)
    setMessage(message)
  }

  const closeSnackbar = () => setIsOpen(false)

  return (
    <SnackbarContext.Provider value={{ showSnackbar }}>
      {children}
      <Snackbar isOpen={isOpen} severity={severity} message={message} onClose={closeSnackbar} />
    </SnackbarContext.Provider>
  )
}

export const useSnackbar = () => useContext(SnackbarContext)

呼び出し側はuseSnackbar()からshowSnackbarを取り出し、重大度とメッセージを渡せばOKです。

const { showSnackbar } = useSnackbar()
showSnackbar('warning', 'message here')

コメント