tamakipedia

日記

【React】ReactMarkdownライブラリを使ってプレビュー機能付きマークダウンエディターを実装する

はてなブログを書いていて気になっていたマークダウンエディタを実装します。

時間

調査 8h
実装 2h
ライブラリのドキュメントを読んだりに費やす時間が多かった。

ツール

使用したツールは下記の通り。

ライブラリ名 概要説明 ドキュメント
ReactMarkdown Markdownを安全にレンダリングするためのコンポーネント https://github.com/remarkjs/react-markdown
remark-gfm GitHub Flavored Markdown(GFM)の拡張機能自動リンク、脚注、取り消し線、テーブル、タスクリスト)をサポートするためのremarkプラグイン https://github.com/remarkjs/remark-gfm

実装方法

1. エディタ用の Textbox を作成する

markdown 用のステートと、入力欄を用意します。

                <textarea
                  className="w-full p-4 h-[80vh]"
                  style={{
                    outline: "none",
                    resize: "none",
                  }}
                  rows={10}
                  value={markdown}
                  onChange={(e) => setMarkdown(e.target.value)}
                  placeholder="ここに Markdown を入力..."
                />

2. プレビュー画面のコンポーネントを作成する

markdown ステートを children として囲む形で ReactMarokdown コンポーネントを用意します。
remarkGfm があると Github のマークダウンで使えているようなマークダウン記法を追加設定なしで扱えるようになります。

                  <ReactMarkdown
                    remarkPlugins={[remarkGfm]}
                    components={{}}
                  >
                    {markdown}
                  </ReactMarkdown>
 

3. HTMLのスタイルを決める

components プロパティでは、各HTMLタグに対応する関数を設定し、その中でchildrenを引数として受け取ることができます。
これにより、プレビュー画面で表示するUIを柔軟に設定できます。

                  <ReactMarkdown
                    remarkPlugins={[remarkGfm]}
                    components={{
                      h1: ({ children }) => (
                        <Typography
                          variant="h1"
                          paddingBottom=".2em"
                          fontSize="1.7em"
                          borderBottom={`1px solid ${pink}`}
                        >
                          {children}
                        </Typography>
                      ),
                     // h2, h3, ..., ul, ol など HTMLタグをキーに設定し、表示したいコードを記述できる。
                  >
                    {markdown}
                  </ReactMarkdown>
 

おしまい

【React】開閉のステートと対になるコンポーネントはProvider化する

TL;DR

Reactで開発する時、開閉のためのステートが散らばりすぎて読めない時あるよね
特にページを跨いで使われるスピナー・モーダル・スナックバーに関するステートはUIと一緒に共通部品化しよう。

サンプル

ステートとUIをcontextにおいて Provider & Hooks として呼び出します。
github.com

Provider を使って開閉のステートとコンポーネントをセットに

下記が createContext を用いた一例です。
複数ページにまたがるisOpenXxxxModalが複製・分散されなくなります。

import { Box, Modal } from "@mui/material";
import { createContext, useContext, useState, ReactNode } from "react";

type ModalContextType = {};

const ModalContext = createContext<ModalContextType | undefined>(undefined);

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [content, setContent] = useState<ReactNode>(null);

  const openModal = (modalContent: ReactNode) => {
    setContent(modalContent);
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
    setContent(null);
  };

  return (
    <ModalContext.Provider value={{ isOpen, content, openModal, closeModal }}>
      {children}
      <Modal open={isOpen} onClose={() => setIsOpen(false)}>
        <Box>{content}</Box>
      </Modal>
    </ModalContext.Provider>
  );
};

export const useModal = () => {
  const context = useContext(ModalContext);
  return context;
};

改修前と改修後の比較

実際に下記の機能を持つコンポーネントを想定します。
1. 初回描画時に取得APIを呼び出す
2. モーダルを開き、ボタンを押すと更新APIを呼び出す

改修前

  • 状態の更新ベースの実装
  • Modalが増えるとステートの命名がより複雑になっていく
import { useState } from "react";
import { Box, Button, Snackbar } from "@mui/material";
import { Loading } from "../components/Loading";
import { DataConfigModal } from "../components/DataConfigModal";

const getData = () => {};
const updateData = () => {};

export const Before = () => {
  const [isOpenLoading, setIsOpenLoading] = useState(false);
  const [isOpenModal, setIsOpenModal] = useState(false);
  const [isOpenSnackbar, setIsOpenSnackbar] = useState(false);
  const [message, setMessage] = useState("");

  const onClickGetDataButton = async () => {
    try {
      setIsOpenLoading(true);
      await getData();
      setMessage("取得に成功しました");
      setIsOpenSnackbar(true);
    } catch (error) {
      setMessage("取得に失敗しました");
      setIsOpenSnackbar(true);
    } finally {
      setIsOpenLoading(false);
    }
  };

  const onClickUpdateDataButton = async () => {
    try {
      setIsOpenLoading(true);
      await updateData();
      setMessage("更新に成功しました");
      setIsOpenSnackbar(true);
    } catch (error) {
      setMessage("更新に失敗しました");
      setIsOpenSnackbar(true);
    } finally {
      setIsOpenLoading(false);
    }
  };

  return (
    <Box
      sx={{
        backgroundColor: "black",
        height: "100vh",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
      }}
    >
      <Button onClick={onClickGetDataButton}>Get Data</Button>

      <Button
        onClick={() => {
          setIsOpenModal(true);
        }}
      >
        Open Modal
      </Button>

      <DataConfigModal
        isOpen={isOpenModal}
        onClose={() => setIsOpenModal(false)}
        updateData={onClickUpdateDataButton}
      />

      <Loading isOpen={isOpenLoading} />

      <Snackbar
        anchorOrigin={{
          vertical: "top",
          horizontal: "center",
        }}
        autoHideDuration={3000}
        open={isOpenSnackbar}
        onClose={() => setIsOpenSnackbar(false)}
      >
        <Box sx={{ color: "white" }}>{message}</Box>
      </Snackbar>
    </Box>
  );
};

改修後 ( Modal, Snackbar, Loading と関連ステートを context で管理)

  • アクションベースの開発 ( stateの更新を意識しなくて良い。open close といった動作ベース )
  • モーダルが増えてもステートの命名を気にしなくて良い。(コンポーネント自体の)
const getData = () => {};
const updateData = () => {};

export const After = () => {
  const { openSnackbar } = useSnackbar();
  const { openModal } = useModal();
  const { openLoading, closeLoading } = useLoading();

  const onClickGetDataButton = async () => {
    try {
      openLoading();
      await getData();

      openSnackbar("取得に成功しました", "success");
    } catch (error) {
      openSnackbar("取得に失敗しました", "success");
    } finally {
      closeLoading();
    }
  };

  const onClickUpdateDataButton = async () => {
    try {
      openLoading();
      await updateData();

      openSnackbar("更新に成功しました", "success");
    } catch (error) {
      openSnackbar("更新に失敗しました", "success");
    } finally {
      closeLoading();
    }
  };

  return (
    <Box
      sx={{
        backgroundColor: "black",
        height: "100vh",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
      }}
    >
      <Button onClick={onClickGetDataButton}>Get Data</Button>

      <Button
        onClick={() => {
          openModal(<DataConfigContent updateData={onClickUpdateDataButton} />);
        }}
      >
        Open Modal
      </Button>
    </Box>
  );
};

boolean のステート達を整理するだけでも可読性が上がる模様。

Atomic Design

現在開発中のサービスにおいて
既存のAtomic Design に沿ったコンポーネント設計をしております。

Atomic Design がよくわからなかったのでメモしていきます。

そもそもatomic Design とは

Atomic design is methodology for creating design systems. There are five distinct levels in atomic design: Atomic design はデザインシステムを構築するための手段(方法論)です。大きさのレベルに応じて5つの要素に分類できます。

Atoms, Molecules, Organisms, Templates, Pages それらは Atoms, Molecules, Organisms, Templates, Pages の五つです。

つまり、Atomic DesignはUIを構成するコンポーネントを5つのコンポーネントレベルに分け、
その五つを組み合わせることで全てのUIを表現する手法のことです。

(参照: https://bradfrost.com/blog/post/atomic-web-design/)

単一責任の法則

「モジュールが持つ責務は一つにすべき」という考え方のこと。
Atomic Design でいうところの Atomに一つの責務を置き、それらを組み合わせてアプリケーションを開発する。

分割統治法

「大きな課題を小さく分割することで課題解決をしていく」という考え方。

  

最近コンポーネント設計をしていて、
単一責任の法則も分割統治法も「コンポーネント一つのタスクを最小限にして問題解決、リファクタしやすくする」
ための基本の考え方みたい。
哲学っぽくて面白い。。

参考

https://zenn.dev/offers/articles/20220523-component-design-best-practice

https://qiita.com/MinoDriven/items/76307b1b066467cbfd6a

【typescript】sortを利用して配列を並び替える

sort()メソッドは、配列の要素をソートする関数のこと。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

一般的なuse case

配列にsort() を実行すると昇順に並び替えてくれます。
定義した配列そのものが変更されているので、破壊的変更が加わっていることになります。

const nameList = ["Batto","Atsuki","chieko","Den"];

console.log(nameList.sort())

//["Atsuki", "Batto", "Chieko", "Den"] 

非破壊的ソート

nameList.sort()だと、nameListそのものの配列の順序が変わってしまいます。
それだとイミュータブルの観点からデバックしにくくなってしまいます。

残余構文を利用して、以下のように書き換え

const nameList = ["Batto","Atsuki","chieko","Den"];

const newNameArray = [...nameArray].sort()

//newNameArray → ["Atsuki", "Batto", "Chieko", "Den"] 
//nameArrayはそのまま

ミューテーションについてはこちらの関数型プログラミングお姉さんの動画がおすすめ(笑)
https://www.youtube.com/watch?v=e-5obm1G_FY

比較関数

実はsort()の第一引数に関数を追加することで並び順を制御することができます。

デフォルトの昇順は以下のような比較関数になります。

nameList.sort((a,b) => a + b) //昇順

そして降順は

nameList.sort((a,b) => a - b) //昇順

配列の中身をオブジェクトにし、特定の値でソート

以下のようなオブジェクトでもソートできるということ。。。すごいですよね、、

const stations = [
  {name: "池袋", users: 55223},
  {name: "新宿", users: 10000},
  {name: "渋谷", users: 300},
  {name: "東京", users: 400000},
]

const stationsOriginal = stations.sort((a, b)=> a.users - b.users)


//結果
const stations = [{
  "name": "渋谷",
  "users": 300
}, {
  "name": "新宿",
  "users": 10000
}, {
  "name": "池袋",
  "users": 55223
}, {
  "name": "東京",
  "users": 400000
}] 

//うおおお

【typescript】配列の二列目以降の取り出し

今まではsplice(num)などを利用して
配列を切り分けて取り出していたのですが、最近はもっと直感的な取り出し方があるみたいです。

まとめて取り出し

配列があります

const smalls = [
  "小動物",
  "小型車",
  "小論文"
];

まとめて取り出すことも可能!!

const [a, b , c] = smalls;
//a = '小動物

スプレッド構文を用いて、一番目以降の配列を取得します。

const [, ...other] = smalls;
//["小型車", "小論文"]

新しい配列を作るときは左辺に代入していくのではなく、右辺で配列を直接作って代入する。

const smallsAnime = [
  "コナン君",
  "小次郎"
]
const newArray= [...smalls(slice(0,2),"小売業"],...smallsAnime )]
//[  "小動物", "小型車", "小売業",  "コナン君", "小次郎"]

直感できでわかりやすい、、これから使ってみよう。

【Typescript】Partial<T> ついて

今日はUtility Typesについて学んだので、その学習メモ。
さまざまなユーティリティーがあるが、その一つを例にとってみると少し理解が深まりました。
今回はPartialについて。

Utilty Types とは

Typescriptが提供する型の関数のようなもの。
元のインターフェースを利用し、新しい型を作成することができる。

Partial

それぞれのプロパティをoptionalにしたいときに利用する。

//インターフェースがあります
interface Todo {
    title: string;
    description: string;
}

//インターフェースTodoをもつtodo1
const todo1: Todo = {
    title: "orgnize desk",
    description: "clear clutter",
}

//アップデートするための関数
function updateTodo(todo: Todo, fieldsTodUpdate: Partial<Todo>){
    return {...todo, ...fieldsTodUpdate};
}

//updateTodoを利用し第一引数にコピーしたいオブジェクト、第二引数に追加したいオブジェクトを追加
const todo2 = updateTodo(todo1, {description: "throw out trash",})

この時、updateTodoの第二引数の型を

Partial<Todo>

にすることで、「Todoのプロパティーのうちどれか」を実現することができます。

終わり

こんな感じで、ユーティリティーを使うと元々のインターフェースを加工した型を表現することが可能です。
他にも特定のインターフェースを減らしたり、増やしたり、レコードを作成したり、色々できるみたいなので
現場のコードと照らし合わせつつ勉強していこうと思います。

参考

https://www.typescriptlang.org/docs/handbook/utility-types.html

【TypeScript】Utility Typesをまとめて理解する - Qiita

【Typescript】型ガードについて

型ガード(Type guard)について

if分やcase文を始めたとした条件分岐で変数の方を判別し、ブロック内の変数の型を絞り込む機能のこと。
型を変数内で明示して変換する「キャスト」を使用することを防げる。

type of

変数の型をチェックできます。

type You = string
const you: You = "kotaro";

console.log(typeof You) 
// string

型がstringとnumberの合併型の場合は、stringにしか使えないメソッドを使うとエラーになる

type NumOrStr = number|string ;

function useSomeNumOrStr (arg:NumOrStr): NumOrStr {
  const toUpperCaseStr = arg.toUpperCase()
  return toUpperCaseStr
} 

//Property 'toUpperCase' does not exist on type 'NumOrStr'.
//Property 'toUpperCase' does not exist on type 'number'.

typeof の結果を条件分岐してあげることでエラーを回避できる

type NumOrStr = number|string ;

function useSomeNumOrStr (arg:NumOrStr): NumOrStr {
  if (typeof arg === "string") {
    const toUpperCaseStr = arg.toUpperCase()
    return toUpperCaseStr
  }
  return arg
}