React: Prop drilling Vs. useContext hook
Understanding how best to pass properties from parent to child component
It is almost impossible to build a React app without having to pass properties, state and global variables around; typically from parent components to children components and even to deeper levels. In this article, I will explain two methods of passing down properties and some use cases. I will also give my opinion about the advantages and disadvantages of both methods.
Prerequisites
JavaScript
ES6
Basic knowledge of React components
What is Prop Drilling?
Prop drilling is a method of passing properties from a top-level component to a component nested within it. Prop drilling is not an official term for this process instead it is an expression that has been adopted in the industry to describe this method of passing properties through nested components.
Consider a list that stores information about patients currently admitted to a hospital.
const data = [
{id: 0, name: 'John Doe', age: 34},
{id: 1, name: 'Tavit Supram', age: 16},
{id: 2, name: 'Omobolanle Tani', age: 42},
{id: 3, name: 'Claire Whay', age: 79},
{id: 4, name: 'Ajan Sare', age: 11},
]
I would use this data to create a patient list where each patient element has a button that removes it from the list:
const App = ()=>{
const [patients, setPatients] = useState(data);
const dischargePatient = (id)=>{
let patientsList = patients.filter(patient => patient.id !== id)
setPatients(patientsList)
}
return (
<AllPatients patients={patients} dischargePatient={dischargePatient}/>
)
}
const AllPatients = ({patients, dischargePatient})=>{
return (
<>
{patients.map((patient) =>{
const { id, name, age} = patient
return <Patient key={patient.id} id={id} name={name} age={age} dischargePatient={dischargePatient} />
})}
</>
)
}
const Patient = ({id, name, age, dischargePatient})=> {
return <>
<h2>{name}</h2>
<p>{age}</p>
<button onClick={()=> dischargePatient(id)}>Discharge</button>
</>
}
export default App;
Explanation:
App
is a root component that will be used to render a list of patients currently admitted to a hospital. It creates a state value for all currently admitted patients, and within it, a function is defined that removes a patient from the list.
However, in real-life cases, the App
component will perform more functionality than this so I will use another component to render the list of patients
AllPatients
component handles this iteration over each patient object. For it to function at all, it is passed two props: the patients
array, and the dischargePatient
function.
Further, on each iteration, AllPatient
returns the Patient
component and delivers the properties to their final destination.
This is a fun example to show how prop drilling works. In this example, you would notice that the AllPatient
component has no use of the properties passed it. It just further passes them down.
Now imagine there are two more levels to go down before getting to the desired component. That will result in a bulky code that ultimately does nothing other than carry properties down to a nested component. To avoid this mess, React has provided us with the context API and useContext
.
useContext
Context is a functionality that provisions a way of passing properties from high-level components to lower levels within it without having to manually declare pass property at each level. This is useful in managing global data like global states, services, settings, themes, etc.
To use context in React, three key steps are involved:
Creation of context
Providing context
Consuming the context.
How to create context:
Context is created in two steps; first, the built-in function createContext is imported and initialized using any of the two syntaxes below.
import React, { useContext, createContext } from 'react';
const PatientContext = createContext("default value");
Alternatively, this syntax does not require importing createContext directly:
import React, { useContext} from ‘react’;
const PatientContext = React.createContext("default value");
In both cases, we need two things, the useContext hook and createContext. We will learn how to use them as we continue.
The data type of PatientContext
is an object, it contains two methods: the provider and the consumer. One way to access these methods is by object destructuring but that is entirely left to you to decide.
Also, the ‘default value’ is stored as the default value of the PatientContext context.
Usage:
Providing the context
The Provider component that is available on instances of the context is used to provide the context to nested components at all levels. It can also take a value prop that sets the value of that context.
<PatientContext.Provider value={{patients, dischargePatient}}>
<AllPatients/>
</PatientContext.Provider>
What this means is that all components that are nested within the PatientContext.Provider
component now has access to the patients
array and the dischargepatient
function.
The value
prop is set and it replaces the default value set initially. In a case where this value
prop is not set, the context takes the default value, and if a default value is not set, the value would be undefined.
Consuming the context
Remember how I imported useContext
earlier, now I will put it to use in every component where I need to access the value of the PatientContext
component.
const AllPatients = ()=>{
const {patients} = useContext(PatientContext);
return (
<>
{patients.map((patient) =>{
return <Patient key={patient.id} {...patient}/>
})}
</>
)
}
Explanation:
patients
array is destructured from PatientContext
context and iterated over. In each iteration, a Patient
component is passed the id
property from the current iteration and a copy of all its other properties.
const Patient = ({id, name, age})=> {
const {dischargePatient} = useContext(PatientContext)
return <>
<h2>{name}</h2>
<p>{age}</p>
<button onClick={()=> dischargePatient(id)}>Discharge</button>
</>
}
Explanation:
dischargePatient
is destructured from the PatientContext
context.
The Patient
component rendered the value of the name
and age
properties to the DOM and set the onClick
of a button accompanying each patient element.
Putting these three steps together, we can refactor the prop drilling example to use the contextAPI as below:
import React, { useContext, useState, createContext } from "react"
import {data} from './data'
const PatientContext = createContext()
const App = ()=>{
const [patients, setPatients] = useState(data);
const dischargePatient = (id)=>{
let patientsList = patients.filter(patient => patient.id !== id)
setPatients(patientsList)
}
return (
<PatientContext.Provider value={{patients, dischargePatient}}>
<AllPatients/>
</PatientContext.Provider>
)
}
const AllPatients = ()=>{
const {patients} = useContext(PatientContext);
return (
<>
{patients.map((patient) =>{
return <Patient key={patient.id} {...patient}/>
})}
</>
)
}
const Patient = ({id, name, age})=> {
const {dischargePatient} = useContext(PatientContext)
return <>
<h2>{name}</h2>
<p>{age}</p>
<button onClick={()=> dischargePatient(id)}>Discharge</button>
</>
}
Consumer method vs. useContext
Although I used useContext
to retrieve the value of PatientsContext
, that is not the only syntax that is applicable.
Using the consumer method, the AllPatients
component above will look like this:
const AllPatients = ()=>{
return (
<PatientContext.Consumer>
{
(PatientContext) => {
{PatientContext.patients.map((patient) =>{
return <Patient key={patient.id} {...patient}/>
})}
}
}
</PatientContext.Consumer>
)
}
Indeed, it is a lot more code and complexity when compared to using useContext
const AllPatients = ()=> {
const {patients} = useContext(PatientContext);
return (
<>
{patients.map((patient) =>{
return <Patient key={patient.id} {...patient}/>
})}
</>
)
How to dynamically update the value of a context
There is no dedicated method to update the context value using the consumer components. So to update the value of a context, you may have to integrate a state management system using the useState or useReducer hook and setting an update function in the context as well.
Advantages and Disadvantages
It is slightly simpler to use props directly. It is manual and you see the progression. But with context, the process is slightly complex and involves a lot of technicalities. Also, context might be more difficult to debug.
Context provides direct access no matter how many levels are there between a parent and child component while passing props one at a time would be clumsy when a lot of components are involved.
Conclusion
This article has been able to explain the concepts of prop drilling and contextAPI. With practice, you can cement the knowledge here gained.
Happy Hacking!