LoginSignup
2
2

React + TypeScript: コンテクストが渡すデータを更新するコード例

Posted at

コンテクストのデータを更新するには、値はプロバイダが定められた親コンポーネントの状態変数にしてください。コンテクストのプロバイダにvalue値として渡すのはその状態変数です。

React公式ドキュメントの「useContext」には「Updating data passed via context」の項で、コンテクストの渡すデータを更新する5つのコードサンプルが紹介されています。もっとも、説明があまりありません(サンプルのコードを見てくださいということのようです)。そこで、本稿では解説をもう少し補ってみましょう。さらに、TypeScriptで型づけし、モジュール分けやコードについても少し工夫を加えました。

チェックボックスによりコンポーネントのカラーを切り替える

つぎのサンプル001では、チェックボックスでコンポーネントのカラーが切り替わります。コードの中身については、「コンテクストが渡すデータを更新する」に解説しましたのでお読みください。

サンプル001■React + TypeScript: useContext 02

コンテクストによりオブジェクトを更新する

以下のサンプル002で、プロバイダのvalueに渡されるオブジェクトがプロパティとして備えるのはふたつの値です。

  • currentUser: 状態を表すオブジェクト({ name: string })
  • setCurrentUser: 状態を設定する関数(() => void)

それぞれ、親コンポーネント(App)の状態変数とその設定関数に定められています。

src/App.tsx
export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
function App() {
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<CurrentUserContext.Provider
			value={{
				currentUser,
				setCurrentUser,
			}}
		>
			<Form />
		</CurrentUserContext.Provider>
	);
}

設定関数(setCurrentUser)により状態変数値(currentUser)を更新するのが、孫コンポーネント(LoginButton)のボタン(Button)です。ただし、設定値は決め打ち({ name: 'Advika' })なので、他の値には変わりません。

src/LoginButton.tsx
export const LoginButton: FC = () => {
	const { currentUser, setCurrentUser } = useContext(CurrentUserContext);
	if (currentUser.name !== '') {
		return <p>You logged in as {currentUser.name}.</p>;
	}
	return (
		<Button
			onClick={() => {
				setCurrentUser({ name: 'Advika' });
			}}
		>
			Log in as Advika
		</Button>
	);
};

サンプル002■React + TypeScript: useContext example 01

複数のコンテクストを定める

以下のルートモジュールsrc/App.tsxがつくるのはふたつのコンテクストです。子コンポーネントは、ふたつのプロバイダ(Provider)に包まれています。

  • ThemeContext: 文字列で定められた要素のカラー(CSS)。
    • デフォルト値: 'light'
  • CurrentUserContext: 現在ログイン中のユーザー名を扱う状態変数と設定関数が収められたオブジェクト。
    • currentUser: 状態変数。
      • { name: string }
    • setCurrentUser: 状態設定関数。
      • (user: { name: string }) => void

ThemeContextの値(theme)を切り替えるのは、チェックボックス(<input type="checkbox">)[Use dark mode]です。

src/App.tsx
export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
function App() {
	const [theme, setTheme] = useState('light');
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			>
				<WelcomePanel />
				<label>
					<input
						type="checkbox"
						checked={theme === 'dark'}
						onChange={({ target: { checked } }) => {
							setTheme(checked ? 'dark' : 'light');
						}}
					/>
					Use dark mode
				</label>
			</CurrentUserContext.Provider>
		</ThemeContext.Provider>
	);
}

モジュールsrc/WelcomePanel.tsxは、ログインユーザー名(currentUser.name)が設定済みかどうかで、表示する子コンポーネントを切り替えます。

src/WelcomePanel.tsx
export const WelcomePanel: FC = () => {
	const { currentUser } = useContext(CurrentUserContext);
	return (
		<Panel title="Welcome">
			{currentUser.name !== '' ? <Greeting /> : <LoginForm />}
		</Panel>
	);
};

設定済みのユーザー名(currentUser.name)を表示するのがsrc/Greeting.tsxモジュールです。

src/Greeting.tsx
export const Greeting = () => {
	const { currentUser } = useContext(CurrentUserContext);
	return <p>You logged in as {currentUser.name}.</p>;
};

ユーザー名が未設定のときは、モジュールsrc/LoginForm.tsxがテキストフィールドで入力を求めます。[Log in]ボタン(<Button>)でユーザー名登録のために呼び出すのは、コンテクスト(CurrentUserContext)から得た状態設定関数(setCurrentUser)です。

src/LoginForm.tsx
export const LoginForm: FC = () => {
	const { setCurrentUser } = useContext(CurrentUserContext);

	return (
		<>

			<Button

				onClick={() => {
					setCurrentUser({
						name: firstName + ' ' + lastName,
					});
				}}
			>
				Log in
			</Button>

		</>
	);
};

コンテクストThemeContextが返す状態変数値themeは、CSSのクラス(className)によって要素のカラーを切り替えます。コンテクストを用いるモジュールsrc/Panel.tsxsrc/Button.tsxのコンポーネントの定めは以下のとおりです。

src/Panel.tsx
export const Panel: FC<Props> = ({ title, children }) => {
	const theme = useContext(ThemeContext);
	const className = 'panel-' + theme;
	return (
		<section className={className}>
			<h1>{title}</h1>
			{children}
		</section>
	);
};
src/Button.tsx
export const Button: FC<Props> = ({ children, disabled, onClick }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return (
		<button className={className} disabled={disabled} onClick={onClick}>
			{children}
		</button>
	);
};

つぎのサンプル003で。ユーザー名(CurrentUserContext)変更の[Log in]ボタンとカラー(ThemeContext)切り替えのチェックボックス[Use dark mode]がそれぞれ働いていることをお確かめください

サンプル003■React + TypeScript: useContext example 02

コンテクストプロバイダをコンポーネントに切り出す

アプリケーションが大きくなると、ルート近くでコンテクストプロバイダが子コンポーネントを幾重にも包むようになるかもしれません。それ自体は気にしなくてよいことです。けれど、プロバイダの入れ子をひとつのコンポーネントとして切り出せば、組み立てが整理できます。

前掲サンプル003のプロバイダは、たかだか二重の入れ子です。でも、この作例で、プロバイダコンポーネントを切り出してみましょう。プロバイダコンポーネントsrc/MyProviders.tsxはつぎのように定めます(import文や型づけおよびコンテクストのデフォルト値の定めは省きました。後掲サンプル004でお確かめください)。基本的には、ルートモジュールsrc/App.tsxからコードを抜き出しただけです。

src/MyProviders.tsx
export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
);
export const MyProviders: FC<Props> = ({ children, theme }) => {
	const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		<ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			>
				{children}
			</CurrentUserContext.Provider>
		</ThemeContext.Provider>
	);
};

すると、ルートモジュールsrc/App.tsxは、コンテクストプロバイダで二重にラップしていた子コンポーネントをプロバイダコンポーネントMyProvidersで包めば済みます。コンテクストをつくるコード(createContext)も要りません。なお、状態変数themeと設定関数setThemeは、Appコンポーネントから子(<input type="checkbox")に渡すため残します。その場合、MyProvidersには状態変数値をプロパティとして渡せばよいのです(前掲MyProvidersコンポーネントがpropsとして受け取っていることをお確かめください)。

src/App.tsx
import { MyProviders } from './MyProviders';

/* export const ThemeContext = createContext('light');

export const CurrentUserContext = createContext<CurrentUserContextType>(
	defaultCurrentUserContext
); */
function App() {
	const [theme, setTheme] = useState('light');
	// const [currentUser, setCurrentUser] = useState({ name: '' });
	return (
		/* <ThemeContext.Provider value={theme}>
			<CurrentUserContext.Provider
				value={{
					currentUser,
					setCurrentUser,
				}}
			> */
		// <MyProviders theme={theme} setTheme={setTheme}>
		<MyProviders theme={theme}>
			<WelcomePanel />
			<label>
				<input
					type="checkbox"
					checked={theme === 'dark'}
					onChange={({ target: { checked } }) => {
						setTheme(checked ? 'dark' : 'light');
					}}
				/>
				Use dark mode
			</label>
		</MyProviders>
		/* </CurrentUserContext.Provider>
		</ThemeContext.Provider> */
	);
}

コンテクストを用いる子コンポーネントは、コンテクストのimportもとパスの書き替えが必要です。

// import { ThemeContext } from './App';
import { ThemeContext } from './MyProviders';
// import { CurrentUserContext } from './App';
import { CurrentUserContext } from './MyProviders';

書き替えたアプリケーションの動きは前掲サンプル003と変わりません(サンプル004)。けれど、プロバイダコンポーネントMyProvidersを切り出したことにより、ルートモジュールsrc/App.tsxのコードが整理されたでしょう。

サンプル004■React + TypeScript: useContext example 03

コンテクストとリデューサで状態に関わるロジックを切り出す

コンテクストにリデューサを組み合わせると、状態に関わるロジックがコンポーネントから切り出せます(「React + TypeScript: 状態のロジックをリデューサに切り出す」参照)。

以下のサンプル005は、Todoリストの作例です。リデューサをプロバイダコンポーネントと組み合わせたコンテクストモジュールsrc/TasksContext.tsxは、つぎのように組み立てました。

src/TasksContext.tsx
const tasksReducer: Reducer<TaskType[], ActionType> = (
	tasks,
	action
) => {
	switch (action.type) {
		case 'added': {

		}
		case 'changed': {

		}
		case 'deleted': {

		}
		default: {

		}
	}
};
export const TasksProvider: FC<Props> = ({ children }) => {
	const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
	return (
		<TasksContext.Provider value={tasks}>
			<TasksDispatchContext.Provider value={dispatch}>
				{children}
			</TasksDispatchContext.Provider>
		</TasksContext.Provider>
	);
};

注目いただきたいのは、コンテクスト(TasksContextTasksDispatchContext)がexportされていないことです。コンポーネントが必要とする状態変数tasksdispatch関数は、カスタムフックuseTasksContextから得られるようにしました。つまり、コンポーネントは直にコンテクストには触れません。

src/TasksContext.tsx
const TasksContext = createContext<TaskType[] | null>(null);
const TasksDispatchContext = createContext<Dispatch<ActionType> | null>(null);

export const useTasksContext = () => {
	const tasks = useContext(TasksContext);
	const dispatch = useContext(TasksDispatchContext);
	return { tasks, dispatch };
};

コンテクストモジュールsrc/TasksContext.tsxの切り出しにより、きわめて簡潔になったルートモジュールsrc/TaskApp.tsxののコードはつぎのとおりです。それぞれのモジュールの記述全体とアプリケーションの動きは、以下のサンプル005でお確かめください。この作例のコードについては、「React + TypeScript: リデューサとコンテクストで拡張性を高める」で段階を踏んで解説しています。

src/TaskApp.tsx
export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

サンプル005■React + TypeScript: Scaling Up with Reducer and Context 04

2
2
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
2
2