I like starting these posts with a bit of introspection about my current learning processes. The technical content of the blog starts here
In previous blogs, I’ve talked about what a great tool blogging is to learn about a process or aspect of programming that I’m struggling to to get a hold of. I’ve found the act of breaking things down and writing them out step by step to be a great to approach a learning problem I’m experiencing at the time.
For this blog though, I’ve decided not to look at something I’m struggling with, but to look at alternatives to a pretty basic aspect of React programming that I do every time I program in it: transferring data between components.
When programming, I often reach out for the easiest and first way I can think of to solve a problem. There’s nothing wrong with that in itself- when you’re just starting out, the most important thing is writing code that works.
In my experience though, there are often multiple ways of solving something. A great use I can see for these blogs is, not just breaking down something I’m struggling with, but exploring alternatives to something that I think I know well, otherwise, it’s hard to ever know I’m using the right tool for the job or just falling back on habits to achieve the task as best they can!
As I said, in this blog, I’ll be looking at a few different ways to pass data to deeply nested components.
Props
Props are such a fundamental part of React programming, it might seem redundant to cover them in any detail. However, when we're later looking at different ways of passing data to nested components, I think it will have been useful to remember the benefits of using props and why they're such a powerful feature of React.
If we look at this basic example:
function MainComponent(){
return(
<FirstNestedComponent/>
)
}
function FirstNestedComponent(){
return(
<SecondNestedComponent/>
)
}
function SecondNestedComponent(){
let greeting = "Hello World"
return (
<h1>{greeting}</h1>
)
}
This example's component hierarchy could be represented something like this:
And it would render something like this:
This is an example of using components without props. By defining the greeting
variable within the SecondNestedComponent
, the component has lost all flexibility. The SecondNestedComponent
can say "Hello World" and nothing else.
Using that component again:
function FirstNestedComponent() {
return (
<>
<SecondNestedComponent/>
<SecondNestedComponent/>
</>
)
};
Would just render the same content twice:
If we instead pass the value for greeting to the SecondNested Components via props:
function FirstNestedComponent() {
return (
<>
<SecondNestedComponent greeting = "Hello World"/>
<SecondNestedComponent greeting = "Howdy!"/>
</>
)
};
function SecondNestedComponent(props){
return (
<h1>{props.greeting}</h1>
)
}
The component can now flexibly render different content, depending on the value that it's been passed:
This basic example illustrates some of the power of component-based programming.
As put in the React Documents: Components let you split the UI into independent, reusable pieces, and think about each piece in isolation..
Passing components data via props, rather than having them hold their own data, increases these qualities of independence and reusability making the components more flexible and 'composable' to the programmer.
That being said, as React projects become more complex, passing information to components via props can become increasingly cumbersome and intricate.
The difficulties of props with deeply nested components
If our React App was working with state and had a few more nested components, it might look something like this:
function MainComponent(){
const [greeting, setGreeting] = useState("Hello world")
return(
<FirstNestedComponent greeting = {greeting}/>
)
}
function FirstNestedComponent(props){
return(
<SecondNestedComponent greeting= {props.greeting}/>
)
}
function SecondNestedComponent(props){
return (
<ThirdNestedComponent greeting = {props.greeting}/>
)
}
function ThirdNestedComponent(props){
return (
<FourthNestedComponent greeting = {props.greeting}/>
)
}
function FourthNestedComponent(props){
return (
<h1>{props.greeting}</h1>
)
}
As state is often defined in the top level component (to give it its fullest possible range across an app), to get the required value of greeting
to our FourthNestedComponent
, we need to pass it as props four times through our component hierarchy, just so one component can use it.
With code as basic as our example, this may not seem like a big problem- but the inefficiency of needing multiple lines of code in multiple components, just for a "Hello World" in our last one, will only get worse and worse as our apps grow in size and complexity.
The act of passing props through multiple components that do not use the values being passed is what's referred to as 'prop-drilling' in the React community and it's the main problem that comes out of using props to pass information.
Up till now, props are the main method of passing data that I've been using- prop drilling is just something I've put up with to get my code to work (I'm sure I'm not alone among beginners!).
As said before though, just because something works doesn't mean it's the best way to do something, so let's look at well-known alternatives and consider their costs and benefits.
One would be to use the useContext hook.
Context
The useContext hook allows us to store the data we need in a separate Context object. Then, after wrapping our components in a special "Provider component", we can access that data in our components when we need to, without having to pass it to them as props (and so avoiding the need of 'prop drilling').
Let's see how this works in practice.
In a new, 'context' file we can create our context object:
// src/context/greeting.js
import React from 'react';
const GreetingContext = React.createContext();
Next, we'll make our provider component. Note that this component receives a 'value' prop which can take the data we want to be passed to our nested component:
// src/context/greeting.js
import React from 'react';
// our context object
const GreetingContext = React.createContext();
// our provider component
function GreetingProvider({ children }) {
const greeting = "Hello world"
return <GreetingContext.Provider value={greeting}>{children}</GreetingContext.Provider>
}
export {GreetingContext, GreetingProvider}
We can now wrap our components with our provider component:
import {GreetingProvider} from 'src/context/greeting'
function MainComponent(){
return(
<GreetingProvider>
<FirstNestedComponent/>
</GreetingProvider>
)
}
All the components wrapped in GreetingProvider
now have access to the GreetingContext
object. They can access this object by calling on the useContext
hook.
Let's demonstrate this in our most nested component:
import { GreetingProvider, GreetingContext } from "src/context/greeting";
function MainComponent(){
return(
<GreetingProvider>
<FirstNestedComponent/>
</GreetingProvider>
)
}
function FirstNestedComponent(){
return(
<SecondNestedComponent />
)
}
function SecondNestedComponent(){
return (
<ThirdNestedComponent/>
)
}
function ThirdNestedComponent(){
return (
<FourthNestedComponent/>
)
}
function FourthNestedComponent(){
const greeting = useContext(GreetingContext);
return (
<h1>{greeting}</h1>
)
}
If we look at webpage that's rendered, we can see that our FourthNestedComponent
is getting our desired value for the its greeting
variable:
Our deeply nested component has gotten the value we want and we haven't had to drill our props.
This seems to have solved our problem, but are there any issues with passing information to our components this way?
The Issues of Using Context
Michael Jackson, the creator of React Router said that the problem of using context to pass data to nested component is that it acts essentially as 'an implicit prop'.
The most obvious problem of the 'implicit state' context gives to components is trying to use them outside of the provider component.
If were to do so in our example app:
function MainComponent(){
return(
<>
<GreetingProvider>
<FirstNestedComponent/>
</GreetingProvider>
<FourthNestedComponent/>
</>
)
}
We get an error:
Another issue of components having these 'implied props' is, as put by the React docs, "it makes component reuse more difficult".
If I were to render two of our component that draws it's values from our context object:
function ThirdNestedComponent(){
return (
<>
<FourthNestedComponent/>
<FourthNestedComponent/>
</>
)
}
It would render the same content twice:
If I wanted a different string to be rendered as my greeting
variable, I could change the context object:
// src/context/greeting.js
import React from 'react';
// our context object
const GreetingContext = React.createContext();
// our provider component
function GreetingProvider({ children }) {
const greeting = "Howdy"
return <GreetingContext.Provider value={greeting}>{children}</GreetingContext.Provider>
}
export {GreetingContext, GreetingProvider}
But, as both the FourthNestedComponents
draw their value from this object, obviously, both of them will render the change:
Our components have lost their flexibility. Using context, I can't have one FourthNestedComponent
take one greeting
variable and the other take another, which was such a powerful feature of using props in our most basic example.
I could use things like default values or conditional rendering to address these issues but we turned to this method originally to clean up our code and reduce its complexity.
By using context instead of props, we've spared ourselves from prop-drilling, but we've affected our components independence, reusability and we exchanged one kind of complexity for another.
Luckily, there's also a third way of passing information to our nested components, which achieved by using a prop of React that Michael Jackson calls 'highly highly underappreciated and underused': the children prop.
Composition
Composition allows us pass the content of our components directly to them as props, mainly by using the 'children' prop. We can see how this works with our earlier, more simple example with just two nested components.
Component composition allows us to "compose" the content of our components when we first write them:
function MainComponent(){
return(
<FirstNestedComponent>
<SecondNestedComponent greeting = {"Hello World"}/>
</FirstNestedComponent>
}
And then pass that content to the nested components via the children
prop:
function FirstNestedComponent({children}){
return(
<>
{children}
</>
)
}
With the whole of our code in place:
function MainComponent(){
return(
<FirstNestedComponent>
<SecondNestedComponent greeting = {"Hello World"}/>
</FirstNestedComponent>
}
function FirstNestedComponent({children}){
return(
<>
{children}
</>
)
}
We can see that our code renders as desired:
Applying these principles with our more complex example:
function MainComponent(){
return(
<FirstNestedComponent>
<SecondNestedComponent>
<ThirdNestedComponent>
<FourthNestedComponent greeting = {"Hello World"}/>
</ThirdNestedComponent>
</SecondNestedComponent>
</FirstNestedComponent>
)
}
function FirstNestedComponent({children}){
return(
<>
{children}
</>
)
}
function SecondNestedComponent({children}){
return (
<>
{children}
</>
)
}
function ThirdNestedComponent({children}){
return (
<>
{children}
</>
)
}
function FourthNestedComponent({greeting}){
return (
<h1>{greeting}</h1>
)
}
Shows just how powerful composition can be. Instead of passing our values as props between multiple component layers, we can pass them directly to the required component.
In the case of the useContext
hook, this came at the price of our components' flexibility. In this case, none of that is lost, we can easily pass different values to components:
function MainComponent(){
return(
<FirstNestedComponent>
<SecondNestedComponent>
<ThirdNestedComponent>
<FourthNestedComponent greeting = {"Hello World"}/>
<FourthNestedComponent greeting = {"Howdy"}/>
</ThirdNestedComponent>
</SecondNestedComponent>
</FirstNestedComponent>
)
}
And have them render different content based on their props:
Composition doesn't just clean up our code without adding complexity, it also adds something by giving us better insight into our components' content.
In our first example, looking at the MainComponent
on its own:
function MainComponent(){
return(
<FirstNestedComponent/>
)
}
would give us no clue as to what components or JSX elements our FirstNestedComponent
contains.
Composition allows us to see exactly what's inside our components as we write them:
function MainComponent(){
return(
<FirstNestedComponent>
<SecondNestedComponent greeting = {"Hello World"}/>
</FirstNestedComponent>
}
function FirstNestedComponent({children}){
return(
<>
{children}
</>
)
}
Instead of reducing our components' flexibility, as in the case of using context, Composition gives us a better sense of what our components contain and a better feeling of how our app is constructed, without the programmer having to travel between files to confirm what's inside their components.
Conclusion
Reviewing what we've covered then:
Props and the ability to pass dynamic data to components are an essential part of what makes React a great framework
The difficulty of using props across complex applications arises when having to pass values through multiple levels of component hierarchy
The cumbersome process of passing props through these component levels is called 'prop drilling'
The
useContext
hook is one possible method of passing data directly to the required component and bypassing the need to drill through propsThe problem with context is that it makes components that derive their data from context objects less flexible and reusable
Context is a better tool for broadcasting global variables across apps than it is for specialised, small, reusable components
Another method of avoiding prop drilling is direct Composition of components and passing them their content through the 'children' prop
This not only allows us to pass the correct props directly to our nested components but it also lets us see the passed content of our components as we write them
Composition can offer the right mix of flexibility, simplicity and visibility to our components and is definitely worth turning to if we need to pass our props through multiple levels of component hierarchy
From my perspective, this has been a really useful working through of the different options available to me when I pass props in React.
Having the time to consider each option in practice and look at the wider community's response to them has been a great exercise and is something I'll definitely return to if I suspect the way that I'm doing something regularly has better alternatives out there! Improvement, after all, is a constant process, not a final destination.
Until next time and thanks for reading!
Top comments (0)