コンテクストのデータを更新するには、値はプロバイダが定められた親コンポーネントの状態変数にしてください。コンテクストのプロバイダにvalue
値として渡すのはその状態変数です。
React公式ドキュメントの「useContext」には「Updating data passed via context」の項で、コンテクストの渡すデータを更新する5つのコードサンプルが紹介されています。もっとも、説明があまりありません(サンプルのコードを見てくださいということのようです)。そこで、本稿では解説をもう少し補ってみましょう。さらに、TypeScriptで型づけし、モジュール分けやコードについても少し工夫を加えました。
チェックボックスによりコンポーネントのカラーを切り替える
つぎのサンプル001では、チェックボックスでコンポーネントのカラーが切り替わります。コードの中身については、「コンテクストが渡すデータを更新する」に解説しましたのでお読みください。
サンプル001■React + TypeScript: useContext 02
コンテクストによりオブジェクトを更新する
以下のサンプル002で、プロバイダのvalue
に渡されるオブジェクトがプロパティとして備えるのはふたつの値です。
-
currentUser
: 状態を表すオブジェクト({ name: string }
) -
setCurrentUser
: 状態を設定する関数(() => void
)
それぞれ、親コンポーネント(App
)の状態変数とその設定関数に定められています。
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' }
)なので、他の値には変わりません。
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]です。
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
)が設定済みかどうかで、表示する子コンポーネントを切り替えます。
export const WelcomePanel: FC = () => {
const { currentUser } = useContext(CurrentUserContext);
return (
<Panel title="Welcome">
{currentUser.name !== '' ? <Greeting /> : <LoginForm />}
</Panel>
);
};
設定済みのユーザー名(currentUser.name
)を表示するのが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
)です。
export const LoginForm: FC = () => {
const { setCurrentUser } = useContext(CurrentUserContext);
return (
<>
<Button
onClick={() => {
setCurrentUser({
name: firstName + ' ' + lastName,
});
}}
>
Log in
</Button>
</>
);
};
コンテクストThemeContext
が返す状態変数値theme
は、CSSのクラス(className
)によって要素のカラーを切り替えます。コンテクストを用いるモジュールsrc/Panel.tsx
とsrc/Button.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>
);
};
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
からコードを抜き出しただけです。
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
として受け取っていることをお確かめください)。
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
は、つぎのように組み立てました。
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>
);
};
注目いただきたいのは、コンテクスト(TasksContext
とTasksDispatchContext
)がexport
されていないことです。コンポーネントが必要とする状態変数tasks
とdispatch
関数は、カスタムフックuseTasksContext
から得られるようにしました。つまり、コンポーネントは直にコンテクストには触れません。
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: リデューサとコンテクストで拡張性を高める」で段階を踏んで解説しています。
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