Posted on under React by Owen Conti.
When people think of context in a React JS app, they often think of global state. "I need to manage the theme my app is set to use, I can use context for this." While this isn't wrong, context can be used for so much more than that use case. I like to think of it as "encapsulated state".
Think of context as a component in your tree that can provide props directly to any child below it. Combine that with hooks, and you can write some clean APIs for your components. Let's take a look at a simple example using a Modal component.
Here's a look at a modal that accepts an
onClose
prop which should be called to close the modal. The modal manages its own transition system, so when closing the modal, the modal's custom
closeModal
needs to be called. After the transition is complete, the passed-in
onClose
prop will be called.
1// inside your Modal component... 2 3const Modal = ({ 4 title, 5 onClose 6}) => { 7 const closeModal = useCallback(() => { 8 // Fake code, pretend to start the closing transition 9 // and call `onClose` when the transition is done10 startCloseTransition(onClose);11 }, [onClose]);12 13 return (14 <ModalOverlay closeModal={closeModal}>15 <ModalContent>16 <ModalHeader title={title} closeModal={closeModal} />17 18 <ModalBody>19 {React.cloneElement(children, { closeModal })}20 </ModalBody>21 </ModalContent>22 </ModalOverlay>23 );24};
Here's the corresponding component that uses the
Modal
component above:
1const SomeComponent = () => { 2 const [modalOpen, setModalOpen] = useState(false); 3 4 return ( 5 <div> 6 <button type="button" onClick={() => setModalOpen(true)}>Open Modal</button> 7 8 {modalOpen ? ( 9 <Modal title="Some Modal" onClose={() => setModalOpen(false)}>10 <SomeComponentModalContent />11 </Modal>12 ) : null}13 </div>14 );15};16 17const SomeComponentModalContent = ({18 closeModal19}) => {20 // `closeModal` is available here because the `Modal`21 // component passes it via the `cloneElement` call22 return (23 <div>24 <p>The content of the modal goes here</p>25 <button type="button" onClick={() => closeModal()}>Close the modal</button>26 </div>27 );28};
The example above is about as simple as I could make it. However, in a real application the button in
SomeComponentModalContent
may be multiple levels deep, and you'd have to pass
closeModal
down through the tree.
My proposal for an improved API is to introduce a
ModalContext
, which will expose the
closeModal
function. Let's start with the context implementation.
1// modal context file... 2 3export const ModalContext = React.createContext(); 4 5export const useModal = () => { 6 return useContext(ModalContext); 7} 8 9export const ModalContextProvider = ({ closeModal, children }) => {10 const context = useMemo(() => {11 return {12 closeModal13 };14 }, [closeModal]);15 16 return (17 <ModalContext.Provider value={context}>{children}</ModalContext.Provider>18 );19};
Note that there's a
useModal
hook in there! That will be important in a moment.
Here's the updated
Modal
component, using the new context:
1// inside your Modal component... 2 3const Modal = ({ 4 onClose 5}) => { 6 const closeModal = useCallback(() => { 7 // Fake code, pretend to start the closing transition 8 // and call `onClose` when the transition is done 9 startCloseTransition(onClose);10 }, [onClose]);11 12 return (13 <ModalContextProvider closeModal={closeModal}>14 <ModalOverlay>15 <ModalContent>16 <ModalHeader />17 18 <ModalBody>19 {children}20 </ModalBody>21 </ModalContent>22 </ModalOverlay>23 </ModalContextProvider>24 );25};
Notice how
ModalOverlay
and
ModalHeader
no longer accept the
closeModal
prop? Since we've exposed the
closeModal
function via context, you no longer have to pass it as a prop anymore. Instead,
ModalOverlay
would look like this:
1// inside ModalOverlay component... 2 3const ModalOverlay = ({ 4 children 5}) => { 6 const { closeModal } = useModal(); 7 8 return ( 9 <div onClick={() => closeModal()}>10 {children}11 </div>12 );13};
You might argue that the example above is worse off because
closeModal
was a prop before, so we've now added a line by using the hook. You would be right, but let's take a look at an example with nested components, the
ModalHeader
component.
1// inside ModalHeader component... 2 3const ModalCloseButton = () => { 4 const {closeModal} = useModal(); 5 6 return ( 7 <button type="button" onClick={() => closeModal()}>X</button> 8 ); 9}10 11const ModalHeader = ({12 title13}) => {14 return (15 <header>16 {title}17 18 <ModalCloseButton />19 </header>20 );21};
By using context here, we can create a generic
ModalCloseButton
component that can be used anywhere within a modal to close the modal. No need to worry about ensuring you have access to the correct props because the
ModalCloseButton
component is responsible for pulling what it needs out of context.
Let's continue by looking at how the parent of the
Modal
component changes.
1// No changes to `SomeComponent`... 2const SomeComponent = () => { 3 const [modalOpen, setModalOpen] = useState(false); 4 return ( 5 <div> 6 <button type="button" onClick={() => setModalOpen(true)}>Open Modal</button> 7 8 {modalOpen ? ( 9 <Modal title="Some Modal" onClose={() => setModalOpen(false)}>10 <SomeComponentModalContent />11 </Modal>12 ) : null}13 </div>14 );15};16 17const SomeComponentModalContent = () => {18 // `SomeComponentModalContent` has access to the modal context19 const {closeModal} = useModal();20 21 return (22 <div>23 <p>The content of the modal goes here</p>24 <button type="button" onClick={() => closeModal()}>Close the modal</button>25 </div>26 );27};
Instead of passing
closeModal
down however many levels of the tree, we leave it to the component needing
closeModal
to grab it out of context.
I've tried to make the examples as simple as possible. In the real world application I pulled this from, the
ModalContext
exposes more than just a
closeModal
function.
We recently built a custom
Stepper
component which allows the user to flow through a series of steps in a form. We used the context/hook pattern to allow custom components in each step to control the flow of the
Stepper
component. The API is so much simpler than prop passing because the custom components only pull what they need out of a hook. Don't need anything? Don't use the hook!
Hopefully you found this article useful! If you did, share it on X!
Found an issue with the article? Submit your edits against the repository.