React Hooks ✕ AtomicDesignで画像投稿webアプリのフロントエンドを実装する

`Figma`と`Storybook`で作成したプロトタイプを実際にReactで実装していきます。
（プロトタイプ、アプリイメージについてはこちらの記事に詳細を投稿しています。）
> [Figma ✕ React Storybook で作るプロトタイピング & スタイルガイド](https://qiita.com/stranger1989/items/65d6ed0668ac717917a2)

Reactに関しては、技術の入れ替わりが激しく「3ヶ月前に採用したモジュールがすでにdeplicatedになっている」なんてことはざらにあります。
そんななか`React 16.8`から導入されたのが、`Hooks`です。公式曰く
> フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。

未だ業務でバリバリ、クラスコンポーネントで書いている私にとっては寝耳に水なアップデートですが、
> React からクラス型コンポーネントを削除する予定はありません。
> フックは既存のコードと併用することができるので、段階的に採用していくことが可能です

とのことだったので少し安心し、ほったらかしにしていたのですが、クラスコンポーネントに対する批判的なコメントが記述してあり、結局のところは移行する方向で進めたほうが良さそうな雰囲気です。
そこで「技術を把握するためには実際に手を動かせ」とばかりに、今さらながらも個人アプリの開発に採用してみました。
Hooks APIには様々な種類のAPIがありますが、基本的にステート管理をする`useState`、副作用を管理する`useEffect`が使いこなせればアプリケーションの開発は可能なので、今回の開発ではシンプルにこの2つのAPIしか使用していません。

# <font color="LightSeaGreen">クラスコンポーネントとHooksとの違い</font>
コンポーネントのライフサイクルにおいて一番使用するであろう、`state`、`componentDidMount`、`componentDidUpdate`、`componentWillUnmount`をクラスコンポーネント、Hooksそれぞれで実装した場合の比較を行っています。
Hooksの方がコードがシンプルなことがわかります。今回のサンプルはわかりやすくするため、`componentDidMount`、`componentDidUpdate`、`componentWillUnmount`の処理を個別に`useEffect`で再現していますが、実際の実装はこれらの処理を1つの`useEffect`にまとめて記述することができます。


## クラスコンポーネント
```typescript

import React from 'react';

interface Props {}

interface State {
  count: number;
}

class AppClass extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    console.log('componentDidMount');
  }

  componentDidUpdate() {
    console.log('componentDidUpdate');
  }

  componentWillUnmount() {
    console.log('componentWillUnmount');
  }

  render() {
    return (
      <>
        {this.state.count}
        <button
          onClick={() => {
            this.setState({count: this.state.count + 1})
          }}
        >
          +
        </button>
        <button
          onClick={() => {
            this.setState({count: this.state.count - 1})
          }}
        >
          -
        </button>
      </>
    );
  }
}

export default AppClass;

```

## Hooks
```typescript

import React, { useState, useEffect } from 'react';

const AppHooks: React.FC = () => {
  const [count, setCount] = useState(0);

  //　コンポーネントマウント時に1回実行
  useEffect(() => {
    console.log('componentDidMount');
  }, []);

  //　コンポーネント更新毎に実行
  useEffect(() => {
    console.log('componentDidUpdate');
  });

  //　コンポーネント更新時、'count'が変更されていた場合実行
  useEffect(() => {
    console.log('componentDidUpdate');
  }, [count]);

  //　コンポーネントアンマウント時に1回実行
  useEffect(() => {
    return () => console.log('componentWillUnmount');
  }, []);

  return (
    <>
      {count}
      <button
        onClick={() => {
          setCount(prevCount => prevCount + 1)
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          setCount(prevCount => prevCount - 1)
        }}
      >
        -
      </button>
    </>
  );
}

export default AppHooks;
```
# <font color="LightSeaGreen">Atomic Designとは</font>
`Atomic Design`とは、コンポーネントの再利用性を向上させるためのデザインシステムで、`React`と相性が良くコンポーネントの設計を考えるときにとても参考になります。
以下の図が`Atomic Design`のステージ図です。`atom`（最小単位）→`pages`（最終型）までコンポーネントの重複を可能な限り0にすることで無駄がなく、テストを実行しやすいアプリケーションを作ることができます。
他の記事でもっとわかりやすいものがあるとは思いますが、自分の理解のため再度概要を記述しておきます。
> ![Atomic_Design_Methodology___Atomic_Design_by_Brad_Frost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/fc6954f7-3b12-97c0-3a9a-5063c30cddab.jpeg)
> [Atomic Design Methodology](http://atomicdesign.bradfrost.com/chapter-2/) から引用

## Atoms（原子）
アプリケーションを構成する最小の単位です。最も再利用されることが想定されています。`ボタン`・`アイコン`・`フォーム`が`atom`に該当します。基本的に原子同様、それ以上分解が不可能なコンポーネントです。
今回開発するアプリは`atom`は独自のものを作成せず全て`Material-ui`のデフォルトのもとを使用することにしました。（数が大量になることが予想され管理が大変なため）
<img width="306" alt="977ff8621999b054033c8c24cbaf63e7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/35369390-de3f-6cb2-a6ea-3ce6c6769673.png">

## Molecules（分子）
原子コンポーネントを組み合わせて作られる、比較的単純なコンポーネントです。こちらも再利用されることが想定されています。`検索フォーム`・`キャプション・いいねボタン付き画像`などが該当します。
<img width="302" alt="99b1d8c1ac988486b0563e21a0b85815.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/84ee82b7-af50-a9f6-a2f8-2cc38013e82d.png">

## Organisms（有機体）
有機体は複数の原子・分子コンポーネントが組み合わさった複雑なコンポーネントです。ここまで来ると再利用性はあまり求められていません。`ヘッダー`・`カード`・`記事`・`フッター`などが該当します。
<img width="1438" alt="b743db4729e76236c6b304e171a5741f.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/0558425f-501e-70ef-821c-fc71a6847853.png">

## Templates（テンプレート）
有機体をレイアウトに配置し、アプリケーションの画面を構成します。ただ、テンプレートの段階では実際のデータは注入せず、ワイヤーフレーム（スケルトン）の状態です。
<img width="794" alt="e8643e2beb8330644183cf862e515b11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/3210f97a-5b52-c45c-4770-ad7c81dda247.png">

## Pages（完成形）
最終的に、ワイヤーフレームに実際の`画像`・`テキスト`などのデータを流し込み、アプリケーションが完成します。
<img width="744" alt="fd794e89c35607235820a267d38d98cc.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/ab454f90-33d4-bb8c-ea19-2d08275f580c.jpeg">


# <font color="LightSeaGreen">Atomic Designを意識したファイル構成</font>
Hooksで実装を行う前に、ファイル構成を考えておく必要があります。React公式ページのDOCS FAQ「[ファイル構成](https://ja.reactjs.org/docs/faq-structure.html)」によれば、ファイル構成についてのベスト・プラクティスは存在せず、プロジェクトに応じて適したファイル構成を考える必要があるようです。（正直ここは定形テンプレートがあれば、考えずに済むのですが、そこは開発者に委ねられているようです。）
私は今回`Figma`と`Storybook`を使って、`AtomicDesign`使用を意識した設計方針で行きたいと考えていたので、以下のようなファイル構成にしました。（非同期処理の管理は`react redux`を使います）

```shell:src以下ファイル構成
src/
├── actions/
│   ├── index.tsx
│   ├── index.d.tsx
│   ├── Auth.tsx
│   └── Album.tsx
├── common/
│   └── image/
├── components/
│   ├── 01_atoms/
│   ├── 02_molecules/
│   │   ├── LoginForm.tsx
│   │   ├── MainTitle.tsx
│   │   ├── PhotoList.tsx
│   │   ├── SearchForm.tsx
│   │   └── Spiner.tsx
│   └── 03_organisms/
│       ├── Auth.tsx
│       ├── Album.tsx
│       ├── Header.tsx
│       ├── ImageUpload.tsx
│       ├── PostForm.tsx
│       └── UpdateForm.tsx
├── containers/
│   ├── App.tsx
│   ├── Album.tsx
│   ├── Header.tsx
│   └── ImageUpload.tsx
├── constants/
│   ├── Auth.tsx
│   ├── Album.tsx
│   └── GlobalUITheme.tsx
├── store/
│   ├── reducers/
│   │   ├── Auth.tsx
│   │   └── Album.tsx
│   └── configureStore.tsx
├── index.tsx
└── serviceWorker.ts
```

`molecules`に分類するか`organisms`に分類するか迷った節もあるので、自己流になってしまいましたが、デザインシステムから大きく乖離していることもないと思います。特に共通`molecules`、`organisms`が繰り返されるリストはどこに割り振るべきか悩みますね。
また、全てのコンポーネントに対してStorybookファイルを記述しているのですが、上記ファイル構成に記述すると冗長になってしまうので省略してあります。

# <font color="LightSeaGreen">コード設計方針</font>
## 全体イメージ図
![redux_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/c863e606-b906-f4eb-1799-07e3480b10ee.png)

## Presentational Component
- UIの**見た目（ビュー）**を担当
- `Atomic Design`においては`atom`・`molecules`・`organisms`部分のコードを記述
- `Container`から受け取ったデータ（props）を表示する（`Container`にデータを渡すことはない）
- `Container`から受け取ったコールバック関数を実行する
- 基本的にはコンポーネントの状態を持たない（`Container`から状態を受け取る）
- 自身のコンポーネントの状態を管理することも稀にある
- CSSも基本的にはコンポーネントに内包する（`CSS-in-JS`）。`material-ui`を使用している場合は`makeStyles`・`createStyles`でスタイルを記述できる。その他のコンポーネントは`emotion`・`styled-components`などで記述。

## Container Component
- UIの**ライフサイクルの管理**、**機能**を担当
- `Atomic Design`においては`templete`・`pages`部分のコードを記述（ビューコンポーネントを組み合わせ、jsonなどのデータを流し込む）
- コンポーネントの状態を持つ
- `redux`アクションを`dispatch`（発送）する
- `redux`における`store`の状態を`map`（対応付）する

## Action
- `redux`による`store`に格納された状態を変更するためのアクションを発行する
- API通信などの非同期処理もここで行う（`middleware`に`redux thunk`を使用した場合）

## Store
- `action`が発行されたタイミングで`action type`を`reducer`が分別し、`store`に格納されている状態が変更される

## Constant
- 定数を定義する

## Common
- 画像、jsonデータなどアプリ全体で使用する共通項目を保存

# <font color="LightSeaGreen">コード実装</font>
全てのコードは載せきれない・説明しきれないので、`アルバムリスト表示`・`画像投稿機能`の2機能に絞ってコードの記述を説明します。
また、今回は`TypeScript`の記事ではないため、型定義に関しては`any`でかなり緩めになっています。堅牢なアプリをつくるためにはコンポーネント間の値の受け渡し時に`interface`でしっかりと型定義を行う必要があります。

## material-uiのカスタムテーマを定義
機能を実装する前に、`material-ui`の`MuiThemeProvider`を使用してでカスタムテーマを定義します。とは言っても、基本的にはデフォルトの設定を活かすつもりですので、カラーの`メインカラー`・`アクセントカラー`のみ定義します。

```typescript:src/constants/GlobalUITheme.tsx
import { createMuiTheme, Theme } from '@material-ui/core/styles';

export const theme: Theme = createMuiTheme({
  palette: {
    primary: {
      light: '#7383A2',
      main: '#445577',
      dark: '#002f6c',
      contrastText: '#FFF',
    },
    secondary: {
      light: '#FF6428',
      main: '#FF9678',
      dark: '#c41c00',
      contrastText: '#FFF',
    },
  },
});

```

```typescript:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './containers/App';
import * as serviceWorker from './serviceWorker';
import { MuiThemeProvider } from '@material-ui/core/styles';
import { theme } from './constants/GlobalUITheme';

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <App />
  </MuiThemeProvider>,
  document.getElementById('root')
);

serviceWorker.unregister();
```

## アルバムリスト表示
### 仕様
投稿したアルバムのリストを表示する機能です。
`Figma`では再現しきれていなかったのですが、画像は複数枚登録可能で横スクロールして2枚目以降の画像を見ることができます。
![0f7bf787fedd12316479a7e277a5f0c3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/9fc25cff-16cb-1872-310a-8463300371fc.gif)

### Hooksによるライフサイクルの流れ

1. `album`コンポーネントを読み込んだ際に、`redux action`によりAPIリクエストが行われ、アルバムデータをAWSから取得
2. コンテナコンポーネントにおいて状態管理、コールバック関数の定義
3. コンテナコンポーネントにおいて`organisms`から`Album`UIコンポーネントを呼び出し、データ・関数を受け渡し
3. `Album`UIコンポーネントにおいて`molecules`から`PhotoList`UIコンポーネントを呼び出し、データを受け渡し

>`PhotoList`は、正確には画像1枚を`molecules`、画像がリストになったものを`organisms`としなければならないのですが、今回は画像リストを`molecules`としました。テストなどで不都合があれば修正するかもしれません。（カードリストも同様です）

> While some organisms might consist of different types of molecules, other organisms might consist of the same molecule repeated over and over again. For instance, visit a category page of almost any e-commerce website and you’ll see a listing of products displayed in some form of grid.
> [Atomic Design Methodology](http://atomicdesign.bradfrost.com/chapter-2/) から引用

### Hooksによるコード

**Container Component**

```typescript:src/containers/Album.tsx
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import Spiner from '../components/02_molecules/Spiner';
import Album from '../components/03_organisms/Album';
import * as albumActions from '../actions/AlbumAPI';

// redux storeの値をstateにマッピング
const mapStateToProps = (state: any): any => ({
  albumState: state.album,
});

// redux actionの読み込み
const mapDispatchToProps = (dispatch: Dispatch): any => {
  return {
    albumActions: bindActionCreators(albumActions, dispatch),
  };
};

const AlbumContainer: React.FC<any> = ({ albumState, albumActions }) => {
  // stateの定義
  const [open, setOpen] = useState(false);
  const [albums, setAlbums] = useState([]);
  const [updateValue, setUpdateValue] = useState([]);

  // albumコンポーネントが読み込まれた際に、アルバムデータ取得アクションを発行
  useEffect(() => {
    (async () => {
      await albumActions.getAlbumListFunc();
    })();
  }, []);

  // API通信で値取得後にredux stateをlocal stateに代入
  useEffect(() => {
    if (albumState.isLoaded) {
      setAlbums(albumState.albums);
    }
  }, [albumState.isLoaded]);

  // モーダルを開くコールバック関数
  const handleClickOpen = () => {
    setOpen(true);
  };

  // モーダルを閉じるコールバック関数
  const handleClose = () => {
    setOpen(false);
  };

  // アルバムを削除するアクションを発行するコールバック関数
  const albumDelete = async (album: any): Promise<void> => {
    await albumActions.deleteAlbumFunc(albums, album);
  };

  // アルバムを更新するアクションを発行するコールバック関数
  const albumUpdate = async (album: any): Promise<void> => {
    setOpen(false);
    await albumActions.updateAlbumFunc(albums, album);
  };

  return albumState.isLoading ? (
    <Spiner />
  ) : (
    <>
      <Album
        open={open}
        handleClickOpen={handleClickOpen}
        handleClose={handleClose}
        albums={albumState.albums}
        albumDelete={albumDelete}
        albumUpdate={albumUpdate}
        setUpdateValue={setUpdateValue}
        updateValue={updateValue}
      />
    </>
  );
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(AlbumContainer);

```

**Presentational Component Organisms**
<img width="955" alt="c2f8877fb39248ab348e353cedc3ee43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/da739c0e-d6b3-3301-8041-d25a0801f59c.png">

```typescript:src/components/03_organisms/Album.tsx
import React from 'react';
import _ from 'lodash';
import moment from 'moment';

import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';

import {
  Container,
  Box,
  Grid,
  Card,
  CardHeader,
  CardContent,
  CardActions,
  Avatar,
  IconButton,
  Typography,
  Dialog,
  DialogContent,
  DialogTitle,
} from '@material-ui/core';

import Delete from '@material-ui/icons/Delete';
import EditIcon from '@material-ui/icons/Edit';
import MoreVertIcon from '@material-ui/icons/MoreVert';

import UpdateForm from './UpdateForm';
import PhotoList from '../02_molecules/PhotoList';

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    card: {
      maxWidth: 350,
      minWidth: 350,
      marginTop: '20px',
    },
    avatar: {
      backgroundColor: theme.palette.grey[700],
    },
  })
);

const Album: React.FC<any> = ({
  open,
  handleClickOpen,
  handleClose,
  albums,
  albumDelete,
  albumUpdate,
  setUpdateValue,
  updateValue,
}) => {
  const classes = useStyles();

  return (
    <>
      <Container maxWidth="lg">
        <Box mt={'70px'}>
          <Grid
            container
            direction="row"
            justify="space-evenly"
            alignItems="flex-start"
          >
            {_.map(albums, (album: any) => {
              return (
                <Card className={classes.card}>
                  <CardHeader
                    avatar={
                      <Avatar aria-label="recipe" className={classes.avatar}>
                        {album.owner}
                      </Avatar>
                    }
                    action={
                      <IconButton aria-label="settings">
                        <MoreVertIcon />
                      </IconButton>
                    }
                    title={album.title}
                    subheader={moment(album.createdAt).format(
                      'YYYY-MM-DD HH:mm'
                    )}
                  />
                  <PhotoList picture={album.picture} />
                  <CardContent>
                    <Typography
                      variant="body2"
                      color="textSecondary"
                      component="p"
                    >
                      {album.note}
                    </Typography>
                  </CardContent>
                  <CardActions disableSpacing>
                    <IconButton
                      aria-label="delete"
                      onClick={() => albumDelete(album)}
                    >
                      <Delete />
                    </IconButton>
                    <IconButton
                      aria-label="edit"
                      onClick={() => {
                        handleClickOpen();
                        setUpdateValue(album);
                      }}
                    >
                      <EditIcon />
                    </IconButton>
                    <Dialog
                      open={open}
                      onClose={handleClose}
                      aria-labelledby="form-dialog-title"
                      maxWidth="sm"
                      fullWidth
                    >
                      <DialogTitle id="form-dialog-title">
                        Update Album
                      </DialogTitle>
                      <DialogContent>
                        <UpdateForm
                          onSubmit={albumUpdate}
                          initialValues={updateValue}
                        />
                      </DialogContent>
                    </Dialog>
                  </CardActions>
                </Card>
              );
            })}
          </Grid>
        </Box>
      </Container>
    </>
  );
};

export default Album;

```

**Presentational Component Molecules**
![fc8f3060f384986577fa2e5ac702332b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/a7b8ffc4-1f3c-0f40-6bf0-85a05222dffb.gif)

```typescript:src/components/02_molecules/PhotoList.tsx
import React from 'react';
import _ from 'lodash';
import { S3Image } from 'aws-amplify-react';

import { GridList, GridListTile, GridListTileBar } from '@material-ui/core';

import IconButton from '@material-ui/core/IconButton';
import StarBorderIcon from '@material-ui/icons/StarBorder';

import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    gridList: {
      flexWrap: 'nowrap',
      transform: 'translateZ(0)',
    },
    title: {
      color: theme.palette.grey[100],
    },
    titleBar: {
      background:
        'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)',
    },
    dummyImage: {
      maxWidth: '100%',
      maxHeight: '100%',
      backgroundColor: 'lightgray',
    },
  })
);

export const PhotoList: React.FC<{ picture: any[]; isMock?: boolean }> = ({
  picture,
  isMock,
}) => {
  const classes = useStyles();

  return (
    <>
      <GridList className={classes.gridList} cols={1}>
        {picture.length === 0 ? (
          <div className={classes.dummyImage} />
        ) : (
          _.map(picture, (tile: any) => (
            <GridListTile key={tile.id}>
              {!isMock ? (
                <S3Image
                  level="public"
                  imgKey={tile.file.key}
                  theme={{
                    photoImg: {
                      maxWidth: '100%',
                      maxHeight: '100%',
                      objectFit: 'cover',
                    },
                  }}
                />
              ) : (
                <img
                  src={tile.file.key}
                  style={{
                    maxWidth: '100%',
                    maxHeight: '100%',
                    objectFit: 'cover',
                  }}
                />
              )}
              <GridListTileBar
                title={tile.name}
                classes={{
                  root: classes.titleBar,
                  title: classes.title,
                }}
                actionIcon={
                  <IconButton aria-label={`star ${tile.name}`}>
                    <StarBorderIcon className={classes.title} />
                  </IconButton>
                }
              />
            </GridListTile>
          ))
        )}
      </GridList>
    </>
  );
};

export default PhotoList;

```



## アルバム投稿機能

### 仕様
複数枚の画像を投稿できる機能です。
ドラックアンドドロップアップロード、アップロード前プレビュー表示を実現させるために、`react-dropzone`を使っています。もちろん投稿スペースをクリックして画像を選択することも可能です。
プレビュー時の画像は一時的にファイルパスとして、stateで状態管理することで実現しています。
![0e35d7c45f764c2d9aa5a8a1bc10797a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/0be3264f-474f-8c29-a2ff-c7c5bdbb2d3e.gif)

### Hooksによるライフサイクルの流れ

1. `ImageUpload`コンポーネントを読み込んだ際に、「編集画面」の場合S3から画像を読み込み。redux stateにファイルパスを追加。「新規画面」のときは何もしない。
2. 画像投稿フィールドに画像がドラックアンドドロップされた際、filesに画像パスを格納。プレビューを表示。
3. リセットボタンが押されたとき、redux state・local state・プレビューパスを全て削除

### Hooksによるコード

**Container Component**

```typescript:src/containers/ImageUpload.tsx
import React, { useState, useEffect } from 'react';

import ImageUpload from '../components/03_organisms/ImageUpload';

const ImageUploadContainer: React.FC<any> = ({ input, reset, resetTriger }) => {
  // stateの定義
  const [updateValue, _] = useState(input);
  const [files, setFiles] = useState([]);

  // fileｓ stateの値が変更されたとき（画像がアップデートされたとき）、プレビューに画像パスをセット
  useEffect(() => {
    if (files.length === 0) {
      if (updateValue.value) input.onChange(updateValue.value);
    } else {
      input.onChange(files);
    }
    files.forEach((file: any) => URL.revokeObjectURL(file.preview));
  }, [files]);

  // resetボタンが押されたとき、redux state、local state、プレビューに格納されたfileを空にする。
  useEffect(() => {
    setFiles([]);
    input.onChange(files);
    files.forEach((file: any) => URL.revokeObjectURL(file.preview));
    resetTriger(false);
  }, [reset]);

  return (
    <ImageUpload files={files} setFiles={setFiles} updateValue={updateValue} />
  );
};

export default ImageUploadContainer;

```

**Presentational Component Molecules**
<img width="880" alt="cf2cbfec90a66344e2745a2c96eba8a5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/5106d6e6-e90a-a0a6-7161-f35b2c864f29.png">

```typescript:src/components/03_organisms/ImageUpload
import React from 'react';
import { S3Image } from 'aws-amplify-react';

import { useDropzone } from 'react-dropzone';

import { Box, Typography } from '@material-ui/core';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import ImageIcon from '@material-ui/icons/Image';

const useStyles = makeStyles(() =>
  createStyles({
    dropBox: {
      marginTop: 20,
      minHeight: '20px',
      padding: 10,
      border: 'dashed 3px lightgray',
      color: 'lightgray',
      textAlign: 'center',
      '&:hover': {
        color: 'gray',
        border: 'dashed 3px gray',
        cursor: 'pointer',
      },
    },
    thumbsContainer: {
      display: 'flex',
      marginTop: 16,
      overflow: 'scroll',
    },
    thumb: {
      display: 'inline-flex',
      borderRadius: 2,
      border: '1px solid #eaeaea',
      marginBottom: 8,
      marginRight: 8,
      width: 100,
      height: 100,
      padding: 4,
    },
    thumbInner: {
      display: 'flex',
      minWidth: 0,
      overflow: 'hidden',
    },
  })
);

const ImageUpload: React.FC<any> = ({ files, setFiles, updateValue }) => {
  const classes = useStyles();

  // プレビューを作成する処理
  const { getRootProps, getInputProps } = useDropzone({
    accept: 'image/*',
    onDrop: (acceptedFiles: any) => {
      setFiles(
        acceptedFiles.map((file: any) =>
          Object.assign(file, {
            preview: URL.createObjectURL(file),
          })
        )
      );
    },
  });

  // サムネイルを作成する処理
  const thumbs = files.map((file: any) => (
    <Box className={classes.thumb} key={file.name}>
      <Box className={classes.thumbInner}>
        <img
          src={file.preview}
          style={{ width: 'auto', height: '100%', display: 'block' }}
          alt="preview"
        />
      </Box>
    </Box>
  ));

  // 「新規」ではなく「編集」の場合AWS S3にアップロードされている画像データを読み込み
  const updateThumbs = updateValue.value
    ? updateValue.value.map((image: any) => (
        <Box className={classes.thumb} key={image.name}>
          <Box className={classes.thumbInner}>
            <S3Image
              level="public"
              imgKey={image.file.key}
              theme={{
                photoImg: {
                  display: 'block',
                  width: 'auto',
                  height: 100,
                },
              }}
            />
          </Box>
        </Box>
      ))
    : null;

  return (
    <>
      <div {...getRootProps({ className: 'dropzone' })}>
        <input {...getInputProps()} />
        <Box className={classes.dropBox}>
          <Typography>
            Drag and drop some files here, or click to select files
          </Typography>
          <ImageIcon fontSize="large" />
          <Box className={classes.thumbsContainer}>
            {files.length === 0 ? updateThumbs : thumbs}
          </Box>
        </Box>
      </div>
    </>
  );
};

export default ImageUpload;

```

# <font color="LightSeaGreen">まとめ</font>
コードファイルが断片的になってしまったので、全体像が若干わかりづらくなってしまいましたが、少しでもHooksの雰囲気を掴んでもらえたら幸いです。
Hooks自体は特段難しいものではなく（もちろん全てのAPIをつかいこなすのはそれなりに大変ですが）クラスコンポーネントでのライフサイクル管理の概要が理解できていればすんなり理解できるようになると思います。むしろコードの記述量が少なくなる分、わかりやすいといえるのではないでしょうか。
AtomicDesignについては`atom`・`molecules`・`organisms`の仕分け方が難しく感じました。どうしても自分の都合の良い風に概念をねじ曲げてしまい自己流になってしまいがちですが、運用するうちに不都合を訂正し、研ぎ澄まされていけばいいなと考えています。いまいちしっくりとこなかったなと思われる方は、ブラッド・フロスト氏の原文[Atomic Design Methodology](http://atomicdesign.bradfrost.com/chapter-2/) を是非読んでもらいたいです。（きれいに概要がまとまっているので読み物としても面白いです。）

今回はフロント部分（コンポーネント）の解説に注力するため、Reduxの最重要部分`action`による`非同期通信`の部分にはあえて触れませんでした。アドベントカレンダーの次回は「`AWS Amplify Auth`によるユーザー認証機能」、「`AWS Amplify AppSync`（GraphQL）によるAPI通信」などのサーバサイド実装についてredux `action` `reducer` `store`と絡めてまとめる予定です。