Help us understand the problem. What is going on with this article?

Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~同期的処理編~

概要

こんにちはよしデブです。
前回(Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~Reduxの準備編~)の続きです。

  1. Reduxを始めるの準備
  2. 同期処理でTodo追加・完了機能を作る(今日はここ)
  3. 非同期処理でログイン機能を作る(メイン)
  4. (おまけ)その他のライブラリ紹介

前回はReduxの準備をしました。今回はこれを使ってTodoリストを実装していこうと思います!
本アプリを実装するにあたり、styled-componentsでコンポーネントのスタイリングをして、Todoの入力フォームにFormikを使用したので、その書き方についても参考になればと思います。

Todoを入力するFormを作る!

では早速Todoリスト追加・完了機能を実装していきましょう!

styled-componentsで部品を作る。

まずはフォームの部品になるものを作っていきます。Atomic Designの考えに則ってAtom→Molecule→Organismの順に作ります。
styled-componentsはコンポーネントのスタイルをcssファイルではなく、JavaScript(TypeScript)内で書けるライブラリです。cssの書き方のままコードに書けちゃいます。CSS in JSの書き方の一つです。

Atom

入力フォーム(TextInput)とラベル(Label)とバリデーションエラーの時に表示するエラーメッセージ(ErrorText)を作りました。
COLOR変数はColor.jsで予め定義した色コードが入っています。TypeScriptでcssが書けるので、cssでは面倒な値の変数化はとても簡単です(scssを使えば変数化もできますね)

src/components/atoms/forms/TextInput.tsx
import COLOR from '@common/Color'
import styled from 'styled-components'

const TextInput = styled.input`
  padding: 10px;
  background: ${COLOR.WHITE};
  border: 1px solid ${COLOR.BLACK};
`

export default TextInput;
src/components/atoms/label/Label.tsx
import styled from 'styled-components'

const Label = styled.div`
  padding-top: 5px;
  padding-bottom: 5px;
`

export default Label
src/components/atoms/forms/ErrorText.tsx
import COLOR from '@common/Color'
import styled from 'styled-components'

const ErrorText = styled.div`
  padding: 5px;
  color: ${COLOR.RED};
`
export default ErrorText;
src/common/Color.ts
const COLOR = {
  PRIMARY_MAIN: '#388E3C',
  PRIMARY_LIGHT: '#60AD5E',
  PRIMARY_DARK: '#124F03',
  SECONDARY_MAIN: '#6E4C40',
  SECONDARY_LIGHT: '#9D786B',
  SECONDARY_DARK: '#40231B',
  WHITE: '#FFFFFF',
  BLACK: '#333333',
  RED: '#F41F20',
}

export default COLOR;

Molecule

TextInputとLabelを組み合わせてラベル付きの入力フォーム(TextField)を作成します。

src/components/molecules/TextField/TextFiels.tsx
import TextInput from '@components/atoms/forms/TextInput'
import Label from '@components/atoms/label/Label'
import { FieldProps } from 'formik'
import React, {FC} from 'react'

interface Props {
  type: string
  label: string
  fieldProps: FieldProps
}

const TextField: FC<Props> = ({fieldProps, label, type }) => {
  const {field} = fieldProps
  return(
    <div>
      <Label>{label}</Label>
      <TextInput type={type} {...field}/>
    </div>
  )
}

export default TextField;

Organism

OrganismでやっとReduxが登場します。

「テキストに入力してボタンを押せばTodoが追加される」 という処理を記述していきます。

1. Formikでフォームを書く

ここで余談ですが、フォームに関しては Formik というフォームライブラリを使用します。よくReduxFormと比較されるようですが、Formik公式はこう言ってます(Why not Redux-form?)

Why not Redux-Form?

By now, you might be thinking, "Why didn't you just use Redux-Form?" Good question.

  1. According to our prophet Dan Abramov, form state is inherently ephemeral and local, so tracking it in Redux (or any kind of Flux library) is unnecessary
  2. Redux-Form calls your entire top-level Redux reducer multiple times ON EVERY SINGLE KEYSTROKE. This is fine for small apps, but as your Redux app grows, input latency will continue to increase if you use Redux-Form.
  3. Redux-Form is 22.5 kB minified gzipped (Formik is 12.7 kB)

My goal with Formik was to create a scalable, performant, form helper with a minimal API that does the really really annoying stuff, and leaves the rest up to you.

要は、 ReduxFormより手軽で動作が軽い らしい。

Formikでのバリデーションチェックはyupというライブラリを使用します。 Schema という形でバリデーションを定義します。 直感的な記述ができて個人的にはとても気に入っています!!
詳しいFormikでのフォームの書き方の説明は今回は割愛します。

2. 追加ボタンが押された時にActionCreatorをdispatchし、Reducerに接続する。

HooksでActionCreatorをdispatchするには useDispatch を使ってReducerと接続した時に呼び出す。たったこれだけ。
Hooksが登場するまでは、Reduxを使いたい時はクラスコンポーネントでconnect関数を使ったりしてましたが、めちゃくちゃスッキリした書き方ができるようになっててびっくりしました!

src/components/organisms/TodoForm/TodoForm.tsx
import ErrorText from '@components/atoms/forms/ErrorText'
import TextField from '@components/molecules/TextField/TextField'
import Button from '@material-ui/core/Button'
import { addTodo } from '@store/todos/actions'
import {
  Field,
  FieldProps,
  Form,
  Formik,
} from 'formik'
import React, {FC} from 'react'
import { useDispatch } from 'react-redux'
import styled from 'styled-components'
import * as Yup from 'yup';


const FieldWrapper = styled.div`
  margin-top: 30px;
  margin-left: 40px;
`

const AddButtonWrapper = styled.div`
  margin-top: 40px;
  margin-left: 40px;
`

// validation schema。yupは便利!
const TodoSchema = Yup.object().shape({
  task: Yup.string()
    .required('入力してください'),
});

export interface TodoFormValues {
  task: string;
}

const TodoForm: FC = ({}) => {
  // hooksの醍醐味。これだけでstoreに接続できる
  const dispatch = useDispatch();
  // 初期値は空文字をセット
  const initialValues: TodoFormValues = { task: '' };

  // ボタンが呼び出されたらaddTodoをdispatch
  // これでreducerにtaskが渡される
  const handleSubmit = (values: TodoFormValues) => {
    dispatch(addTodo(values.task));
  }

  return(
    <Formik
      initialValues={initialValues}
      validationSchema={TodoSchema}
      // type="submit"の要素が押された時に呼び出す関数を設定
      onSubmit={handleSubmit}
      render={({errors, touched}) => (
        <Form>
          <Field
            name="task"
            // renderでinput要素を与える
            render={(props: FieldProps) => {
              return (
                <FieldWrapper>
                  <TextField
                    label={'タスク'}
                    type={'text'}
                    fieldProps={props}
                  />
                  {errors.task && touched.task &&
                  <ErrorText>
                    {errors.task}
                  </ErrorText>}
                </FieldWrapper>
              )
            }}
          />
          <AddButtonWrapper>
            <Button type="submit" variant="contained" color="primary">追加</Button>
          </AddButtonWrapper>
        </Form>
      )}
    />)
};

export default TodoForm;

ページを作成する!

最後にページを作成します。Next.jsではpages配下にページを書くと勝手にルーティングをしてくれます。
index.tsxでは現在のtodoリストとTodo入力フォームを表示します。
ReduxのStateを取得するには useSelector を使用します。
今回は現在のtodoリスト(todos)を useSelector を使用して取得します。

また、ここでは「各todoに対して完了ボタンを押すと完了状態にする」処理を実現します。完了ボタンが押された時に doneTodoアクションをdispatchするように記述してあげます。

pages/index.tsx
import LoginForm from '@components/organisms/LoginForm/LoginForm'
import TodoForm from '@components/organisms/TodoForm/TodoForm'
import { Button } from '@material-ui/core'
import { StoreState } from '@store/index'
import { doneTodo } from '@store/todos/actions'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import Container from '@material-ui/core/Container'

/**
 * TopPage
 */
const TopPage = () => {
  const dispatch = useDispatch()
  // 現在のtodoリストを取得
  const [todos] = useSelector((state: StoreState) => [
    state.todos.todos,
  ])

  return (
    <main>
      <Container maxWidth="xs">
        <h1>Hello, World</h1>
        <h2>Todos</h2>
        // 現在のTodoリストを表示
        <ul>
          {todos.map((todo, idx) => (
            <li key={idx}>
              // doneだったら打ち消し線
              <span
                style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
              >
                {todo.task}
              </span>
              <Button
                variant="contained"
                color="primary"
                // doneだったらボタンを押せなくする
                disabled={todo.done}
                // 完了ボタンを押したらdoneTodoアクションをdispatchする
                onClick={() => dispatch(doneTodo(todo.id))}
                style={{ marginLeft: 10 }}
              >
                DONE
              </Button>
            </li>
          ))}
        </ul>
        <TodoForm />
      </Container>
    </main>
  )
}

export default TopPage

完成!!

ここまでの進捗でこんな感じの簡単なTodoリスト管理ができました。
demo2.gif

終わりに

Redux Hooksを使ったTodoリストの追加・完了機能を実装しました。
ここまでで実装してきたものは同期的処理になります。処理が順番になされるので比較的直感的な記述ができたと思います。

次回は本シリーズのメイン!ログイン機能で非同期処理をする方法をRedux Hooksを使って実現したいと思います。

前回はこちらNext.js × TypeScriptの同期・非同期処理をHooksを使って書く ~Reduxの準備編~
次回はこちらNext.js × TypeScriptの同期・非同期処理をHooksを使って書く ~非同期処理編~

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away