1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React + TypeScript: レンダープロップ

Posted at

React公式サイトに、「レンダープロップ」というテクニックが紹介されています。コンポーネントのプロパティ(多くの場合名前はrender)として関数を渡し、返されるのはJSXのReact要素です。すると、ロジックの実装とJSXのレンダーが切り分けられます。そうすることで、ロジックを使い回しやすくしようという工夫です。

レンダープロップは使わずに簡単なカウンターをつくる

もっとも、公式サイトの説明やコードは、少しわかりにくく、すぐに試せるかたちではありませんでした。そこで、もっとありがちで簡単な、カウンターの作例を採り上げましょう。ここでは、まだレンダープロップは使いません。加算と減算のボタンで数値を増減するだけの例です(コード001)。

コード001■ありがちなカウンターアプリケーション

src/CounterDisplay.tsx
import { FC, MouseEventHandler } from 'react';

type Props = {
	counter: {
		count: number;
		decrement: MouseEventHandler<HTMLButtonElement>;
		increment: MouseEventHandler<HTMLButtonElement>;
	};
};
export const CounterDisplay: FC<Props> = ({
	counter: { count, decrement, increment }
}) => {
	return (
		<div>
			<button onClick={decrement}>-</button>
			<span>{count}</span>
			<button onClick={increment}>+</button>
		</div>
	);
};
src/App.tsx
import { useState } from "react";
import { CounterDisplay } from './CounterDisplay';
import './styles.css';

function App() {
	const [count, setCount] = useState(0);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return (
		<div className='App'>
			<CounterDisplay counter={{ count, decrement, increment }} />
		</div>
	);
}
export default App;

Reactアプリケーションのひな形はCreate React Appでつくり、TypeScriptも加えました(「Create React AppでTypeScriptが加わったひな形アプリケーションをつくる」参照)。なお、Reactのバージョンはv18です。動きが確かめられるように、サンプル001をCodeSandboxに公開したのでご参照ください。

サンプル001■React + TypeScript: Render props 01

とにかくレンダープロップを使ってみる

レンダープロップは、子コンポーネントにプロパティ(名前はrenderが多い)として渡す、戻り値がJSXの関数です。つまり、子コンポーネントがレンダーするReact要素は親が決めることになります。前掲コード001のルートモジュール(src/App.tsx)から、子コンポーネント(CounterDisplay)にレンダープロップ(render)を渡してみましょう。

src/App.tsx
export default function App() {

	return (
		<div className="App">
			{/* <CounterDisplay counter={{ count, decrement, increment }} /> */}
			<CounterDisplay
				render={() => (
					<div>
						<button onClick={decrement}>-</button>
						<span>{count}</span>
						<button onClick={increment}>+</button>
					</div>
				)}
			/>
		</div>
	);
}

子のモジュール(src/CounterDisplay.tsx)は、親から受け取ったレンダープロップ(render)の関数を呼び出してJSXに差し込むだけです。子コンポーネント(CounterDisplay)は、具体的にレンダーするReact要素には関わらなくなりました。

src/CounterDisplay.tsx
// import { FC, MouseEventHandler } from 'react';
import { FC, MouseEventHandler, ReactNode } from 'react';

type Props = {
	/* counter: {
		count: number;
		decrement: MouseEventHandler<HTMLButtonElement>;
		increment: MouseEventHandler<HTMLButtonElement>;
	}; */
	render: () => ReactNode;
};
export const CounterDisplay: FC<Props> = ({
	// counter: { count, decrement, increment }
	render
}) => {
	return (
		<div>
			{/* <button onClick={decrement}>-</button>
			<span>{count}</span>
			<button onClick={increment}>+</button> */}
			{render()}
		</div>
	);
};

書き替えたふたつのモジュールの記述全体は、つぎのコード002のとおりです(サンプル002)。子のモジュール(src/CounterDisplay.tsx)はJSXの組み立てから切り離されたものの、ほかにやることがありません。逆に、ルートモジュール(src/App.tsx)は、ロジックまで丸抱えです。

コード002■とりあえずレンダープロップを使って書き替えた例

src/App.tsx
import { useState } from 'react';
import { CounterDisplay } from './CounterDisplay';
import './styles.css';

function App() {
	const [count, setCount] = useState(0);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return (
		<div className="App">
		<CounterDisplay render={() => (
				<div>
					<button onClick={decrement}>-</button>
					<span>{count}</span>
					<button onClick={increment}>+</button>
				</div>
			)} />
		</div>
	);
}
export default App;
src/CounterDisplay.tsx
import { FC, ReactNode } from 'react';

type Props = {
	render: () => ReactNode;
};
export const CounterDisplay: FC<Props> = ({ render }) => {
	return <div>{render()}</div>;
};

サンプル002■React + TypeScript: Render props 02

レンダープロップでロジックを切り分ける

では、ロジックをルートモジュールsrc/App.tsxから子(CounterDisplay)に移しましょう。状態や振る舞いは、つねにアプリケーション全体でもたなければならないわけではありません。たとえばこのカウンターの例では、そのときどきの増減値や処理の関数は、アプリケーションと共有しなくてよい場合もあります。

けれど、その状態を共有したいコンポーネントや要素が他にある場合が考えどろこです。通常であれば、子コンポーネントのJSXに組み入れてプロパティとして状態を渡せば済みます。ところが、レンダープロップでは受け取り側がJSXを決められません。

そこで思い出さなければならないのは、レンダープロップは関数だということです。子にプロパティが与えられなくても、引数なら渡せます。レンダープロップを定める側が、その引数を適切にJSXのReact要素に割り振ればよいのです。

src/App.tsx
// import { useState } from 'react';

function App() {
	/* const [count, setCount] = useState(0);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1); */
	return (
		<div className="App">
			{/* <CounterDisplay render={() => ( */}
			<CounterDisplay render={(count, decrement, increment) => (

			)} />
		</div>
	);
}

受け取ったレンダープロップの関数(render)を呼び出す側(src/CounterDisplay.tsx)は、JSXがどうレンダーされるか気にすることなく、引数に定められた参照や関数が他のコンポーネントや要素と共有できます。

src/CounterDisplay.tsx
// import { FC, MouseEventHandler, ReactNode } from 'react';
import { FC, MouseEventHandler, ReactNode, useState } from 'react';

type Props = {
	// render: () => ReactNode;
	render: (
		count: number,
		decrement: MouseEventHandler<HTMLButtonElement>,
		increment: MouseEventHandler<HTMLButtonElement>
	) => ReactNode;
};
export const CounterDisplay: FC<Props> = ({ render }) => {
	const [count, setCount] = useState(0);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	// return <div>{render()}</div>;
	return <div>{render(count, decrement, increment)}</div>;
};

親コンポーネントがJXSの組み立て、子コンポーネントはそのツリー内のロジックの提供と役割が切り分けられました(コード003)。モジュールごとのコードと具体的な動きはサンプル003でお確かめください。

コード003■親コンポーネントが組み立てたJSXで子コンポーネントはロジックを担う

src/App.tsx
import { CounterDisplay } from './CounterDisplay';
import './styles.css';

export default function App() {
	return (
		<div className="App">
			<CounterDisplay
				render={(count, decrement, increment) => (
					<div>
						<button onClick={decrement}>-</button>
						<span>{count}</span>
						<button onClick={increment}>+</button>
					</div>
				)}
			/>
		</div>
	);
}
src/CounterDisplay.tsx
import { FC, ReactNode, useState } from 'react';

type Props = {
	render: (
		count: number,
		decriment: MouseEventHandler<HTMLButtonElement>,
		incriment: MouseEventHandler<HTMLButtonElement>,
	) => ReactNode;
}
export const CounterDisplay: FC<Props> = ({render}) => {
	const [count, setCount] = useState(0);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return (
		<div>
			{render(count, decrement, increment)}
		</div>
	);
}

サンプル003■React + TypeScript: Render props 03

カスタムフックを使う

今は、関数コンポーネントが多く使われるようになりました。レンダープロップはカスタムフックに置き替えられる場合が少なくありません。

今回のカウンターにカスタムフックを用いると、組み立ては少し変わってきます。ルートモジュールがJSXを定めるのでなく、子コンポーネントに切り出すかたちです。けれど、ロジックはさらにカスタムフックに切り分けられます。フックを使えればその方が、役割分担はわかりやすいでしょう。詳しい考え方については、「React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で」をお読みください。この記事はTypeScriptを採り入れていなかったので、改めてつくった作例がつぎに掲げたサンプル004です。

サンプル004■React + TypeScript: Using hooks

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?