DEV Community

Cover image for A Matter of Flexibility- Passing Data Between React Components
Angus Bower-Brown
Angus Bower-Brown

Posted on • Updated on

A Matter of Flexibility- Passing Data Between React Components

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

This example's component hierarchy could be represented something like this:

Graphical depiction of a main component and two nested ones

And it would render something like this:

A webpage with an h1 header with the text "Hello World"

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/>
    </>
  )
};
Enter fullscreen mode Exit fullscreen mode

Would just render the same content twice:

Two h1 elements with a "Hello World" string as their content

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

The component can now flexibly render different content, depending on the value that it's been passed:

A webpage with two h1 elements with text content of "Hello World" and "Howdy"

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

Graphical depiction of the hierarchy of a main component and four nested ones

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();


Enter fullscreen mode Exit fullscreen mode

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}


Enter fullscreen mode Exit fullscreen mode

We can now wrap our components with our provider component:

import {GreetingProvider} from 'src/context/greeting'

function MainComponent(){

    return(
        <GreetingProvider>
            <FirstNestedComponent/>
        </GreetingProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

If we look at webpage that's rendered, we can see that our FourthNestedComponent is getting our desired value for the its greeting variable:

A correctly rendered h1 element with "Hello World" as its content

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'.

As he puts it: 'It is implied, when we render [a component using context] that we are in this state and if we are not, then we have to handle it somehow'.

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/>
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

We get an error:

Error message describing a failure to destructure an absent context object

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/>
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

It would render the same content twice:

Two components with the content of "hello world"

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}


Enter fullscreen mode Exit fullscreen mode

But, as both the FourthNestedComponents draw their value from this object, obviously, both of them will render the change:

Two h1 elements saying "Howdy"

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>
}

Enter fullscreen mode Exit fullscreen mode

And then pass that content to the nested components via the children prop:


function FirstNestedComponent({children}){
    return(
        <>
            {children}
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

With the whole of our code in place:

function MainComponent(){
    return(
        <FirstNestedComponent>
            <SecondNestedComponent greeting = {"Hello World"}/>
        </FirstNestedComponent>
}

function FirstNestedComponent({children}){
    return(
        <>
            {children}
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

We can see that our code renders as desired:

One h1 element saying "Hello World

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>

    )
}

Enter fullscreen mode Exit fullscreen mode

And have them render different content based on their props:

One h1 element saying "Hello World" and one saying "Howdy"

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/>
    )
}
Enter fullscreen mode Exit fullscreen mode

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}
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

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 props

  • The 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)