メインコンテンツまでスキップ

「React」タグの記事が5件件あります

全てのタグを見る

· 約8分
山本

こんにちは。サービス開発部プロダクトエンジニアの山本です。

今回は16タイプマッピングという機能開発において、バックエンドを介さずフロントエンドのみでPDF生成を実装した経験についてお話しします。

PDFを生成する場合、一般的にバックエンドを介して処理を行いますが、今回の開発ではバックエンドを使わずフロントエンドのみで実装する必要がありました。

既存のReactコンポーネントをそのままPDF化したかったためです。本記事では、html2canvasとjsPDFを活用し、フロントエンドのみでPDF生成を実装した事例について紹介します。

背景

16type16type分布図

ミキワメ適性検査では16タイプマッピングという機能を提供しています。

上記のようにミキワメの性格検査の結果から16にタイプ分類をして、会社全体や部署ごとに性格特性の分布図を作成することが可能です。

チームビルディングなどに活用され好評を博している機能なのですが、ここにPDFダウンロード機能を追加しました。

課題


ミキワメ適性検査のフロントエンドはRailsのテンプレートエンジンであるhamlからReactへ移行中のため、その2つが混在している状況です。(2025/01 時点)

hamlからのPDF生成はすでに実装していましたが、Reactで作成された画面を直接PDF化した実績はありませんでした。

Reactで書かれたコンポーネントをPDF生成用にhamlで再作成し、従来通りバックエンドを介して実装することも検討しましたが工数がかかること、せっかくReactに移行したのにまたhaml書くんかいという気持ちがあり、もっと手軽にPDF生成する手段を模索することになりました。

技術選定


以下の条件で選定を進めました。

  • 導入コストが低い
  • 実装し易い
  • 主流ブラウザで動作可能
  • React Emotionが期待通り動作する

バックエンドを介さずPDF生成をしたいという需要は一定あると思うのですが、意外と選択肢が少なく、結論としてはhtml2canvasとjsPDFを組み合わせて採用することになりました。

html2canvasはHTML要素をキャプチャし画像化することが出来、jsPDFは画像からPDF生成が可能で、いずれもJavaScriptライブラリです。

react-pdfはJSXで直接レイアウトを記述できる点が魅力的でしたが、React v17までの対応に留まっており、ミキワメ適性検査が採用しているReact v18では動作しませんでした。また、Emotionの互換性もないため、今回の要件には適合しませんでした。

選定条件html2canvas + jsPDFreact-pdf
導入コストが低い✅ 導入が容易✅ 比較的容易
実装し易い✅ シンプルなAPI✅ JSXで直感的に記述可能
主流ブラウザで動作可能✅ Chrome, Firefox, Edge, Safari✅ Chrome, Firefox, Edge, Safari
React Emotionが動作✅ 影響なし❌ 互換性なし React v18に未対応
バックエンド不要でPDF生成✅ 可能✅ 可能
レイアウトの自由度⚠️ HTMLレンダリングに依存✅ JSXで柔軟に記述可能
PDFのカスタマイズ性✅ 縦横・サイズ指定可能✅ 詳細なスタイル調整可能

直面した課題


  • 一部のスタイルが反映されない
  • ブラウザ上でレンダリングが必要なため、表示の工夫が必要
  • 既存機能と一貫性のあるダウンロード操作を実装する必要がある

解決策


  • スタイルの問題は、デザイナーと相談し、許容範囲を判断
  • 一旦非表示エリアにPDF生成用コンポーネントを表示し、見えない形でPDFを生成
  • 既存のダウンロードフローと統一し、ユーザーのUXを維持

デモで使用した実装


シンプルですが以下でブラウザに表示させたPDF生成用コンポーネントをユーザが視認できないよう調整しました。

  1. ダウンロードボタン押下をトリガーにPDF生成用コンポーネントをブラウザに表示
  2. PDF生成、ダウンロード処理
  3. 完了後非表示にする

最終的な実装ではベースレイアウト外でPDF生成用コンポーネントを表示させ、視認のリスクを軽減させています。

export const PdfDownloadButton = ({ className }: Props) => {
const [showPDF, setShowPDF] = useState(false); // PDF生成用コンポーネントを表示する状態管理

// ボタンクリック時にPDF生成用コンポーネントを表示
const handleClick = () => {
setShowPDF(true);
};

// showPDF が true になったら PDF を生成し、完了後に元の状態に戻す
useEffect(() => {
if (showPDF) {
PdfDownload(); // PDF生成処理を実行
setShowPDF(false); // PDF生成が終わったら状態を元に戻す
}
}, [showPDF]);

return (
<div>
{/* PDFダウンロードボタン */}
<Button onClick={handleClick} className={className}>
PDFダウンロード
</Button>
{/* PDF生成用コンポーネント(表示されるとPDFを作成) */}
{showPDF && <PdfTargetComponent />}
</div>
);
};

// PDFを生成する関数
const PdfDownload = () => {
const target = document.getElementById('pdf-target'); // PDF対象の要素を取得
if (target === null) return;

// 取得した要素をPDFとして生成
generatePdf(target);
};

まとめ


フロントエンドのみでPDF生成を行う方法として、html2canvas + jsPDFは有力な選択肢のひとつです。特に、既存のバックエンドを改修せずにPDFを導入したい場合や、Reactで作成されたコンポーネントをそのままPDF化したい場合に有効です。一方で、スタイルの反映やブラウザレンダリングの工夫が必要になるため、導入前に許容範囲を整理することが重要です。

· 約5分
森田 有貴

森田です。

普段は何かしらの機能の実装の話をしてますが、今回は毛色の異なるお話をしようかと思います。

では早速こちらのコードをご覧ください。

import { useEffect, useState } from "react";

type Address = {
address1: string;
address2: string;
address3: string;
kana1: string;
kana2: string;
kana3: string;
prefcode: string;
zipcode: string;
};

export const App = () => {
const [address, setAddress] = useState<Address>();
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060"); //fetchでデータを取得
const data = await response.json();

setAddress(data.results[0]); //data.results[0]をaddressにセット
setLoading(false); //fetchが終わったらloadingをfalseにする
};

fetchData();
}, []);

if (loading) {
return <div>loading...</div>;
} else {
return <div></div>;
}
};

やっていることとしてページをロードした際に指定した郵便番号からどこの住所のものなのかを取得しているだけです。

fetch中はloading...と表示し、終わったら取得した内容を表示するといったページです。

では問題です。addressの型は何でしょうか。

 

 

 

 

  〜シンキングタイム〜  

 

 

 

   

正解は...

Address | undifnedです!正解できましたかね?

そうなんです、useStateで初期値を設定していないのでundefinedがくっついてしまうんです。

なのでaddressの値を表示させようとすると{address?.address1}とオプショナルプロパティをつけて書かないといけなくなってしまうのです。これはよろしくないですね。

これをtypescriptの良さを活かして解決してみましょう!

解決方法としてはaddressを定義する際にその型を(Address & { loading: false }) | { loading: true }とし、その初期値を{ loading: true }とするというものです。

つまり

const [address, setAddress] = useState<(Address & { loading: false }) | { loading: true }>({ loading: true });

とするということです。

で、fetchしたデータをaddressに代入する際にloadingの値をfalseにします。

こうすることによってaddress値はundefinedにならなくなり、さらに別でloadingの値を設定する必要がなくなるのです。

文面だけだと何言ってるか少しよくわからないですね。

最初のコードを実際に編集してみましょう。

import { useEffect, useState } from "react";

type Address = {
address1: string;
address2: string;
address3: string;
kana1: string;
kana2: string;
kana3: string;
prefcode: string;
zipcode: string;
};

export const App = () => {
const [address, setAddress] = useState<(Address & { loading: false }) | { loading: true }>({ loading: true });

useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060"); //fetchでデータを取得
const data = await response.json();
setAddress({ ...data.results[0], loading: false }); //data.results[0]とloading:falseをセット
};

fetchData();
}, []);

if (address.loading) {
return <div>loading...</div>;
} else {
return <div>{address.address1}</div>;
}
};

このようにaddress.loading === falseになっている時だけaddressの値を表示するようにすると通常通り{address.address1}と表示できるようになるというわけです!

めちゃめちゃtypescriptの良さを活かしたtypescriptだからこそできる手法ではないでしょうか。

ちなみにこちらはチームのメンバーさんが見つけてくださったうひょさんの記事を参考にしています。 https://qiita.com/uhyo/items/d74af1d8c109af43849e

実際に社内のプロダクトでもこの手法を取り入れて開発をしています。

もしよかったら参考にしてみてください!

では。

· 約10分
森田 有貴

森田です。 先日 BlockNote について紹介しましたが、今回は BlockNote と yjs と Liveblocks を用いた共同編集機能の実装をご紹介します。

共同編集と聞くと難しそうに感じますが、そんなことは全くなくとても簡単に実装できます。

早速実装といきたいところですが、少しだけ yjs と Liveblocks について説明を...

yjs とは

yjs は CRDT(Conflict-free Replicated Data Type)という技術を持ちたフレームワークです。つまりコンフリクトしないデータの型を扱うことができます。

詳しくはこちら ↓

https://docs.yjs.dev/

Liveblocks とは

Liveblocks はリアルタイムでの共同編集やカーソル共有などのコラボレーション機能を簡単に追加できるツールキットです。

公式ドキュメントがとても充実しているので詳しくはこちらを参照ください ↓

https://liveblocks.io/docs

準備

Nextjs を作成。今回は App Router + Typescript でいきます。そのほかは好みで設定してもらえればいいと思います。

$ npx create-next-app collaborative-editor
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

Liveblocks 周りの設定

Liveblocks 関連のライブラリをインストールします

npm i @liveblocks/client @liveblocks/react @liveblocks/node @liveblocks/yjs

liveblocks を初期化

npx create-liveblocks-app@latest --init --framework react

何やら質問されるので、y で実行しましょう。

$ npx create-liveblocks-app@latest --init --framework react
Need to install the following packages:
create-liveblocks-app@2.20240816.0
Ok to proceed? (y) y

▀█████▀ ▄
▀██▀ ▄██▄
▀ ▄█████▄

Liveblocks


✨ liveblocks.config.ts has been generated!

これでアプリ側の設定は完了。

続いて API キーを取得しにいきましょう。

まずこちらからサインアップhttps://liveblocks.io/

API keys を選択

image1

Generate key...をクリックして

image2

キーを生成!

image3

Public key と Secret key は後々使うのでメモしておきましょう。

BlockNote の設定

先日の記事と同じように blocknote のライブラリもインポートしておきましょう

npm i @blocknote/core @blocknote/react @blocknote/mantine

実装

ではいよいよ実装に入っていきましょう。

まず先程取得した API キーを環境変数に設定しておきましょう。

/.env.local
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= LIVEBLOCKS_SECRET_KEY=

続いてバックエンドを作成します。

/src/app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";
import { NextResponse } from "next/server";
import { nanoid } from "nanoid";

const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});

export async function POST() {
const liveSession = liveblocks.prepareSession(nanoid());

liveSession.allow("*", liveSession.FULL_ACCESS);

const { body, status } = await liveSession.authorize();
return new NextResponse(body, { status });
}

続いてLiveblocks のライブラリを用いてルームの作成接続などの機能のコンポーネントを作成します。

/src/app/Components/Room.tsx
"use client";

import { LiveblocksProvider, RoomProvider, ClientSideSuspense } from "@liveblocks/react/suspense";
import { ReactNode } from "react";

type Props = {
children: ReactNode;
};

export const Room = ({ children }: Props) => {
return (
<LiveblocksProvider authEndpoint={"/api/liveblocks-auth"}>
<RoomProvider id="test-room">
<ClientSideSuspense fallback={<div>...loading</div>}>{children}</ClientSideSuspense>
</RoomProvider>
</LiveblocksProvider>
);
};

authEndpointは先程作成したバックエンドのエンドポイントを設定しましょう。

次にBlockNoteにyjsを組み込んだエディタを作成します。

/src/app/Components/Editor.tsx
"use client";
import { useEffect, useState } from "react";
import { BlockNoteEditor } from "@blocknote/core";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import * as Y from "yjs";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import { useRoom } from "@liveblocks/react/suspense";
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";

type EditorProps = {
doc: Y.Doc;
provider: any;
};

export function Editor() {
const room = useRoom();
const [doc, setDoc] = useState<Y.Doc>();
const [provider, setProvider] = useState<any>();

useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
setDoc(yDoc);
setProvider(yProvider);

return () => {
yDoc?.destroy();
yProvider?.destroy();
};
}, [room]);

if (!doc || !provider) {
return null;
}

return <BlockNote doc={doc} provider={provider} />;
}

function BlockNote({ doc, provider }: EditorProps) {
const editor: BlockNoteEditor = useCreateBlockNote({
collaboration: {
provider,
fragment: doc.getXmlFragment("document-store"),
user: {
name: "User",
color: "#ff0000",
},
},
});

return <BlockNoteView editor={editor} />;
}

作成したコンポーネントをページに組み込んで...

/src/app/collaborative-editing/page.tsx
import { Room } from "../Components/Room";
import { Editor } from "../Components/Editor";

const Page = () => {
return (
<Room>
<Editor />
</Room>
);
};

export default Page;

はい完成🎉

video1

ちょっと簡単すぎじゃないですかね。

おまけ

ルームの接続設定について

RoomコンポーネントのRoomProviderのidでルーム選択ができるようになっています。

/src/app/Components/Room.tsx:13

<RoomProvider id="test-room">

今回はtest-roomになっていますね。

また Liveblocks のダッシュボードから作成されたルームを管理することもできます。

image4

test-roomが作成されていますね。

またルームの id に制約をかけることもできます。

/src/app/api/liveblocks-auth/route.ts:12
liveSession.allow("room:*", liveSession.FULL_ACCESS);

このようにroom:*とするとルーム id にroom:がついていないと接続できないようになります。

今ルーム id をtest-roomにしているのでそのままでは接続できないようになっているはずです。

room:test-roomにすると再度接続できるようになるということです。

接続中の表示

ClientSideSuspensefallbackで接続中に表示されるないようを変更することができます。以下のように変更してみましょう。

/src/app/Components/Room.tsx:14
<ClientSideSuspense fallback={<div>...読み込み中</div>}>{children}</ClientSideSuspense>

video2.gif

任意のコンポーネントで指定できるので、ローディングでよくあるクルクルするやつなどを表示するのもいいかもしれないですね。

ユーザー名と色の変更

相手に表示される自分のユーザー名と色を変更できるようにしてみましょう。

以下でを設定しています。

/src/app/Components/Editor.tsx:46
user: {
name: "User",
color: "#ff0000",
},

レンダリング時に設定されてないといけないので、それも踏まえてEditorコンポーネントを編集してみましょう。

/src/app/Components/Editor.tsx
"use client";
import { useEffect, useState } from "react";
import { BlockNoteEditor } from "@blocknote/core";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import * as Y from "yjs";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import { useRoom } from "@liveblocks/react/suspense";
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";

type EditorProps = {
doc: Y.Doc;
provider: any;
name: string;
color: string;
};

export function Editor() {
const room = useRoom();
const [doc, setDoc] = useState<Y.Doc>();
const [provider, setProvider] = useState<any>();
const [name, setName] = useState("");
const [color, setColor] = useState("");
const [showEditor, setShwoEditor] = useState(false);

useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
setDoc(yDoc);
setProvider(yProvider);

return () => {
yDoc?.destroy();
yProvider?.destroy();
};
}, [room]);

if (!doc || !provider) {
return null;
}

if (showEditor) {
return (
<BlockNote doc={doc} provider={provider} name={name} color={color} />
);
} else {
return (
<div style={{ display: "flex", justifyItems: "center" }}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前"
/>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
type="color"
/>
<button onClick={() => setShwoEditor((prev) => !prev)}>接続</button>
</div>
);
}
}

function BlockNote({ doc, provider, name, color }: EditorProps) {
const editor: BlockNoteEditor = useCreateBlockNote({
collaboration: {
provider,
fragment: doc.getXmlFragment("document-store"),
user: {
name: name,
color: color,
},
},
});

return <BlockNoteView editor={editor} />;
}

名前と色を入力したらテキストを変種できるようにしたという感じですね。

video3.gif

というわけで実装できました。

おわり

今回は共同編集の実装を紹介しました。 Liveblocksはテキストの編集以外にも相手のカーソル表示やリアルタイムの通知機能など様々な機能が用意されています。 yjsについてもMap型やArray型などあり、使い方によって様々に活用できると思うので、ぜひいろいろ試していただければと思います。

今回テキストエディタはBlockNoteを使用しましたが、他のエディタでもほとんど同様に実装できるようになっています。この辺りの話はLiveblocksのドキュメントを参照してみてください。

ではまた。

· 約9分
森田 有貴

森田です。

現在開発しているプロダクトで BlockNote というリッチテキストエディタを使う機会があったのですが、これがめちゃめちゃ便利だったので今回はその紹介をしたいと思います!

BlockNote とは?

BlockNote は React で Notion のようなブロックベースのテキストエディタを実装できるライブラリです。デフォルトの状態で Notion に引けを取らない UX が設定されており、とても簡単に高クオリティなテキストエディタを実装できます。

https://www.blocknotejs.org/

実装

長々と説明するより実際に見てもらうほうが早いと思うので、早速で実装してみましょう! 今回もNextjsで行きます

サクッとプロジェクトを作成して...

$ npx create-next-app blocknote-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

必要なライブラリをインポート

npm install @blocknote/core @blocknote/react @blocknote/mantine

エディタ用のコンポーネントを作成して...

/src/app/page.tsx
"use client";

import "@blocknote/core/fonts/inter.css";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";

export const Editor = () => {
const editor = useCreateBlockNote();
return (
<div>
<BlockNoteView editor={editor} />
</div>
);
};

ページに表示するだけ!

/src/app/page.tsx
import { Editor } from "./components/Editor";

const Page = () => {
return (
<>
<Editor />
</>
);
};

export default Page;

これだけで実装完了!

video

とっても簡単にNotionのようなテキストエディタを実装できてしまいました🎉🎉🎉

できること

blocknoteでは文字を入力できるのはもちろん、文字のサイズや色の変更、画像や動画などさまざまなコンテンツを入力表示させることができます。

image1

image2

入力欄の左の+をクリックすると入力できるものリストが表示されます。

基本的にはMarkdown記法と同様に#でHeadingにしたり、 - でリスト表示にしたりという感じで入力できます。また、+の横の点々の部分をクリックすると文字の色や背景色なども選ぶことができます。

画像や動画などはurlを入力することによって表示させることができます。

さらにBlockNoteという名前の通り、入力したコンテンツをブロックとして移動させることも簡単にできてしまいます。

video2

ものすごい機能が充実していますね😲

スタイル変更

スタイルもかなり自由に変更できるようになっています。 いくつか抜粋してご紹介します。

文字サイズ変更

初期設定では文字のサイズが大きすぎるので変更してみましょう。

.editor-custom-style .bn-editor h1 {
font-size: 18px;
}

.editor-custom-style .bn-editor h2 {
font-size: 16px;
}

.editor-custom-style .bn-editor h3 {
font-size: 14px;
}

.editor-custom-style .bn-editor {
font-size: 14px;
}

cssインポートしてクラスを指定すると...

import "./editor.css";

export const Editor = () => {
const editor = useCreateBlockNote();

return (
<div>
<BlockNoteView editor={editor} className="editor-custom-style" />
</div>
);
};

image3

いい感じのサイズに変更できました!

文字サイズ以外にも色々カスタマイズができるようになっています。

テーマ

続いてテーマを編集してましょう! 背景色を赤色にしてみます。

import { BlockNoteView, darkDefaultTheme } from "@blocknote/mantine";
import { Theme } from "@blocknote/mantine";

export const Editor = () => {
const editor = useCreateBlockNote();

const lightRedTheme = {
colors: {
editor: {
background: "red",
},
},
} satisfies Theme;

const redTheme = {
light: lightRedTheme,
dark: darkDefaultTheme,
};

return (
<div>
<BlockNoteView editor={editor} className="editor-custom-style" theme={redTheme} />
</div>
);
};

image4

むちゃくちゃ目に悪そうですね。

みてわかる通りライトテーマとダークテーマそれぞれ設定できるようになっています。 今回はライトテーマのみにカスタムテーマを当てています。

こちらもかなり自由度高くカスタマイズできるようになっています。

詳しくはこちら↓

https://www.blocknotejs.org/docs/styling-theming/themes

言語変更

デフォルトではプレイスホルダーやメニューの部分の言語が英語になっています。 日本語の設定が用意されているので変更してみましょう。

import { locales } from "@blocknote/core";

export const Editor = () => {
const editor = useCreateBlockNote({
dictionary: locales.ja,
});

return (
<div>
<BlockNoteView editor={editor} className="editor-custom-style" />
</div>
);
};

localesをインポートしてlocales.jaを設定するだけ。

image5

特に変な日本語になっている部分もないですね。

おまけ

入力した内容を取得する

textareaタグのonChange={(e) => console.log(e.target.value)}的なことをしてみましょう!

Reactのテキストエディタですからね。内容が取得できてなんぼですからね。

入力された内容はeditor.documentで取得ができます。また、inputやtextareaと同様にonChangeで入力内容の変化を感知できます。

export const Editor = () => {
const editor = useCreateBlockNote();

const onChange = () => {
console.log(editor.document);
};

return (
<div>
<BlockNoteView editor={editor} onChange={onChange} />
</div>
);
};

video3

というわけで入力が取得できました。

ただ、みてわかる通りこの実装方法ではBlockNote特有のBlock型で出力されてしまいます。

なのでマークダウンに変換して出力してみましょう!

blocksToMarkdownLossyを使ってonChangeを以下のように変更します

const onChange = async () => {
const markdown = await editor.blocksToMarkdownLossy(editor.document);
console.log(markdown);
};

video4

マークダウンで出力されるようになりました!

プログラム側から入力する

続いてプログラム側から内容を入力する方法をご紹介します。

こちらはinputやtextareaとは全く異なります。

export const Editor = () => {
const editor = useCreateBlockNote();

const markdown = `# Hello, world!`;

const loadMarkdown = async () => {
const blocks = await editor.tryParseMarkdownToBlocks(markdown);
editor.replaceBlocks(editor.document, blocks);
};

return (
<div>
<BlockNoteView editor={editor} />
<button onClick={() => loadMarkdown()}>読み込む</button>
</div>
);
};

loadMarkdown関数で入力するマークダウンの文字列をBlock型に変換し、editor.replaceBlocksで置き換えているという感じです。 

実際に動かしてみるとこんな感じ。

video5

replace以外にinsertなどさまざまな操作が可能になっています。

詳しくはこちら↓

https://www.blocknotejs.org/docs/editor-api/manipulating-blocks

おわり

今回はBlockNoteをご紹介しました。今回紹介できたのはほんの一部で他にも便利な機能が沢山用意されているので、ぜひ実際に使っていただければと思います!

公式のDiscordサーバーも活発に動いており、日々質問や不具合報告など飛び交っています。わからないことがあればこちらで聞くといいかもしれないですね。

機会があれば実際にBlockNoteを用いた機能開発も紹介したいと思っています!

ではまた。

· 約6分
山田 哲也

みなさん、こんにちは! 山田です。

前回の記事で Figma を使ってボタンコンポーネントを作ってみました。
今回はそのデザインを元に React と Tailwind CSS で実装していきたいと思います。

開発環境の準備

今回開発環境に Nextjs を選びました。
Next.js はセットアップ時に Tailwind CSS のインストールを選択できるので、すぐに開発を始められます。

セットアップ時の質問は全てデフォルトで進めていきます。

> npx create-next-app@latest
Need to install the following packages:
create-next-app@14.0.4
Ok to proceed? (y) y
✔ What is your project named? … button-ui-example
✔ Would you like to use TypeScript? … No / Yes # Yesを選択
✔ Would you like to use ESLint? … No / Yes # Yesを選択
✔ Would you like to use Tailwind CSS? … No / Yes # Yesを選択
✔ Would you like to use `src/` directory? … No / Yes # Noを選択
✔ Would you like to use App Router? (recommended) … No / Yes # Yesを選択
✔ Would you like to customize the default import alias (@/*)? … No / Yes # Noを選択

Next.js のセットアップが完了したら、プロジェクトのディレクトリに移動しておきます。

UI の確認は Storybook で行うので、こちらもセットアップします。

npx storybook@latest init

セットアップが完了すると、自動的に Storybook が起動します。

Storybook

プリセットでいくつかコンポーネントが用意されていますが、不要なので削除しておきます。

rm -rf stories/*

Storybook の設定

次に Storybook の設定を行います。

簡単なボタンコンポーネントのファイルを作成し、以下の内容で保存します。

// app/components/Button.tsx
import { ReactNode, FC } from "react";

type Props = {
children: ReactNode;
};

export const Button: FC<Props> = ({ children }) => {
return <button>{children}</button>;
};

次に、このコンポーネントを Storybook で確認できるようにします。

// stories/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "../app/components/Button";

const meta = {
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
children: "ボタン",
},
};

Storybook を確認すると、ボタンコンポーネントが表示されます。

meta オブジェクトの tags に autodocs を指定することで、コンポーネントのドキュメントを自動生成することができます。
また、parameters に layout: "centered"を指定することで、コンポーネントを中央に表示することができるので覚えておくと便利です。

Storybook

最後に preview.js に globals.css を読み込ませます。
この時 globals.css に記載されている@tailwind xxxx以外の内容は不要なので削除しておきます。

// .storybook/preview.js
import type { Preview } from "@storybook/react";
import "../app/globals.css";

(省略)

これで Tailwind CSS によるスタイルが Storybook に反映されるようになりました。

ボタンコンポーネントの実装

Figma でデザインしたボタンコンポーネントにはいくつかのバリアントがありました。
ボタンコンポーネントに渡すプロパティの値によってバリアントを変化させるのが一般的かと思いますが、プロパティの数が多くなってくると、条件分岐を独自に実装するのはかなり面倒です。

そこで、「Class Variance Authority」というライブラリを使って、複数のバリアントを持つコンポーネントを簡単に実装できるようにします。

まずはライブラリをインストールします。

npm install class-variance-authority

次に、Button コンポーネントを以下のように修正します。

// app/components/Button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { ButtonHTMLAttributes, FC } from "react";

const button = cva("text-white rounded-md", {
variants: {
intent: {
primary: "bg-blue-500 hover:bg-blue-600",
secondary: "bg-pink-500 hover:bg-pink-600",
},
size: {
sm: "px-2 py-1 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
},
},
defaultVariants: {
intent: "primary",
size: "md",
},
});

export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof button> {}

export const Button: FC<ButtonProps> = ({
className,
intent,
size,
children,
...props
}) => {
return (
<button className={button({ intent, size, className })} {...props}>
{children}
</button>
);
};

cva メソッド利用してバリアントの型を構築していきます。
第一引数に共通で適用されるクラス、第二引数にバリアントとそのバリアントに適用されるクラスを指定することができます。
また、defaultVariants を追加することで、デフォルトで表示されるバリアントを指定することができます。

(なんて便利なんだ...)

では、最後に Storybook でコンポーネントを確認してみましょう。

Storybook

うまく切り替えができているようですね 🎉

以上でボタンコンポーネントの実装は完了です。
複数のバリアントを持つコンポーネントを実装する際は、ぜひこのライブラリを使ってみてください。

それではまた次回!