I am looking for the most concise way, in React, to provide context to a component (say a “page”) and all its subcomponents. For now, the most concise way I have found includes 6 files (!), plus one file per subcomponent that needs the context.
Important notice: I am separating a lot of stuff instead of putting everything in the same file because I want to avoid the following warning:
Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
Here is my MWE in the case where our main component FooPage has 2 subcomponents FooButton and FooText, so, in this case, we have 8 files in total:
File #1: define the type of the context value.
// types.ts
// define the type of the value provided by the context,
// it depends on which stuff we want to provide to the subcomponents
export type FooContextType = {
foo: number;
setFoo: React.Dispatch<React.SetStateAction<number>>;
};
File #2: the actual outer component that we will use in our web page.
// FooPageWrapper.tsx
// This component is the one we actually use, it wraps
// the FooPage inside the context provider.
import FooProvider from "./context/FooProvider";
import FooPage from "./FooPage";
export default function FooPageWrapper() {
return (
<FooProvider>
<FooPage />
</FooProvider>
);
}
File #3: the actual content (which needs to be wrapped in the context provider).
// FooPage.tsx
// This component is the one with the actual content.
// It must be used inside the context provider FooProvider.
import FooButton from "./components/FooButton";
import FooText from "./components/FooText";
export default function FooPage() {
return (
<div>
<h1>Foo Page</h1>
<FooButton />
<FooText />
</div>
);
}
File #4: the definition of the context itself.
// context/FooContext.tsx
// defines the context.
import { createContext } from "react";
import { FooContextType } from "../types";
export const FooContext = createContext<FooContextType | undefined>(undefined);
File #5: the context provider, in which we will put all our logic (this is the most important one).
// context/FooProvider.tsx
// This is the actual context provider component.
// Here we provide the context value using useState,
// but we could provide a more complex value if needed,
// using useReducer or other state management solutions.
import { useState } from "react";
import { FooContext } from "./FooContext";
export default function FooProvider({
children,
}: {
children: React.ReactNode;
}) {
const [foo, setFoo] = useState(0);
return (
<FooContext.Provider value={{ foo, setFoo }}>
{children}
</FooContext.Provider>
);
}
File #6: a custom hook to easily access the context value in our subcomponents.
// context/useFoo.tsx
// Custom hook to use the context FooContext
// when we are in a component that is wrapped by the FooProvider.
import { useContext } from "react";
import { FooContext } from "./FooContext";
export default function useFoo() {
const c = useContext(FooContext);
if (c === undefined) {
throw new Error("FooContext is undefined");
}
return c;
}
File #7: a first subcomponent that uses the context.
// components/FooButton.tsx
// Example of a component that uses the context FooContext
// (via the custom hook useFoo).
import useFoo from "../context/useFoo";
export default function FooButton() {
const { foo, setFoo } = useFoo();
return (
<button onClick={() => setFoo(foo + 1)}>
Foo is {foo}, click to increment it!
</button>
);
}
File #8: a second subcomponent that uses the context.
// components/FooText.tsx
// Example of a component that uses the context FooContext
// (via the custom hook useFoo).
import useFoo from "../context/useFoo";
export default function FooText() {
const { foo } = useFoo();
return (
<span>
Foo is {foo}, this is a text component that uses the Foo context!
</span>
);
}
The example works as expected: we obtain a page with a button and a text, which both can read and write the value of foo in the context. However, this is very verbose.
Is there any way to make it more concise?