React Material の初期開発で<Dialog>いっぱい書きたくない話

###### 概要

dialogやsnackbarをmaterialで記載する時にcontextを利用すると再利用生があがるので、初期開発の速度が上がる可能性がある。

###### 経緯

個人的にも昔のProjectで<Dialog>、[ダイアログ空いてる？、] = useState()てきなことを大量にかいていた戒め。

## TL;DR

ReactのClasss Componentを利用してContext Providerを作成。
component側からは、DialogのReactElementを渡す。
ClassComponentを使わない場合については、hooksが利用できないので(hooks in hooksでエラー)死んだ話。

## 流れ

1. Reactのプロジェクトを作成
2. Materialを追加
3. Class Componentを利用してContextを作成
4. Dialogの中身を作成(適当に)
5. Functional Componen側からDialogContextを呼ぶ

### 1. Reactのプロジェクトを作成

Nxまたはcreate-react-appを利用してアプリケーションを作成

create-react-appを利用する場合

```terminal
$ create-react-app my-app
```

nxを利用する場合
```terminal
$ npx create-nx-workspace@latest --preset=react
```

プロジェクトが作成されたら、Materialの追加

### 2. Materialを追加

```terminal
$ yarn add @material-ui/core
```

ここで下準備終了。

### 3. Class Componentを利用してContextを作成

stateにDialogに必要なopenのflag, elementを利用、
contextValueのcreate, closeのfuncitonを持たせる。
dialogにoptionを追加した場合は、default のoptionを内部で持ちcreateの時にoverrideするといろいろ楽に対応できます。


```contexts/dialog-context.tsx
import { Dialog } from '@material-ui/core';
import React, { createContext } from 'react';

// ?はdefaultValueの時に渡せないため、?を利用
export interface DialogContextValue {
  createDialog?: (elm: JSX.Element) => void,
  closeDialog?: () => void,
  open: boolean
}

/* eslint-disable-next-line */
export interface DialogContexttProps {}

interface DialogContextState {
  open: boolean,
  elm: JSX.Element
}

export const DialogContext = createContext<DialogContextValue>({
  open: false
})

export class DialogcontextProvider extends React.Component<DialogContexttProps, DialogContextState> {
  constructor(props) {
    super(props)
    this.state = {
      open: false,
      elm: null
    }
  }

  createDialog = (elm: JSX.Element) => {
    this.setState(state => ({
      ...state,
      open: true,
      elm
    }))
  }

  closeDialog = () => {
    this.setState(state => ({
      ...state,
      open: false,
      elm: null
    }))
  }

  render() {
    return (
      <DialogContext.Provider value={{
        open: this.state.open,
        createDialog: this.createDialog,
        closeDialog: this.closeDialog
      }}>
        { this.props.children }
        <Dialog open={this.state.open}>
          { this.state.elm }
        </Dialog>
      </DialogContext.Provider>
    )
  }
}

export default DialogcontextProvider;

```

次にcontextを利用するため、Appに追記します。
適宜利用してください。

```app.tsx
import React from 'react';

import DialogcontextProvider from './contexts/dialog-contextt';

const App = () => (
  <div>
    <DialogcontextProvider>
      <div>sample</div>
    </DialogcontextProvider>
  </div>
)

export default App
```

### 4. Dialogの中身を作成(適当に)

これは、適当でいいですがhooksを内部で利用するとclass Componentじゃないといけないことがわかると思います。
useForm, useDispatch, useSelectorなど結構メジャーどころを使ってcontextProvider ClassComponentじゃない時に、hook in hookのエラーが表示されます。

```components/dialog-content.tsx
import React from 'react';

/* eslint-disable-next-line */
export interface DialogContentProps {}

export function DialogContent(props: DialogContentProps) {
  return (
    <div>
      <h1>Welcome to dialog-content!</h1>
    </div>
  );
}

export default DialogContent;
```

### 5. Functional Componen側からDialogContextを呼ぶ

最後に呼び出し元のcomponentを作成して、dialog呼べるか確認していきます。

``````components/button.tsx
import { DialogContent } from './dialog-content';
import React, { useContext } from 'react';
import { DialogContext } from '../contexts/dialog-contextt';

/* eslint-disable-next-line */
export interface ButtonProps {}

export function Button(props: ButtonProps) {
  const { createDialog } = useContext(DialogContext)
  return (
    <div>
      <button onClick={() => createDialog(<DialogContent />)}>click to open</button>
    </div>
  );
}

export default Button;

```

app.tsxに呼び出し元を追加。

```app.tsx
import React from 'react';
import Button from './components/button';

import DialogcontextProvider from './contexts/dialog-contextt';

const App = () => (
  <div>
    <DialogcontextProvider>
      <div>sample</div>
      <Button />
    </DialogcontextProvider>
  </div>
)

export default App
```

表示されたbuttonをクリックした時にelementが表示されます。


![dialog-none.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144543/3c1fdfe0-239f-e7f4-4a2a-2e4df93643c0.png)

![opened-dialog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144543/743ed5bc-ef98-b598-8517-0825cbb62972.png)

地味に利用手段が増えるので、snackbarとかも、入れておくと、

```Sample.tsx
function Sample () {
  useEffect(() => {
      createSnackBar({ type: 'error', message: "Api Failっぽいよ" })
  }, [errors])
  return (
  )
}
```

の様にcomponent側から呼び出すことが可能になり、Componentは一つのため、開発速度は上がります。

[Repo](https://github.com/Ntakuya/react-material-dialog-snackbar/tree/feature/dialog-context)