Maxime Heckel
11 May 2018
•
6 min read
Part One Here
I’ve received a lot of good feedback after publishing my first article about React sub-components, however, some of them got me thinking about how I could further improve the sub-components pattern in order to make it easier to read and use.
Here are some the critiques I got back from some readers:
findByType
for every component using sub-components is annoyingWhile I agreed with all these statements, I couldn’t find an elegant way to address them without making the component difficult to use. However one day, one user from the Reactiflux community mentioned that using contexts would remove the necessity of using the findByType
util within each sub-component; which obviously got me curious. Moreover, I was hearing a lot about the upcoming new Context API in React 16.3.0 and I thought that this would be a great way to start experimenting a bit with this new functionality.
Up until now, I’ve always thought contexts in React were hard to use, and it never felt natural to me to implement components using them except in some rare higher-order components. Plus it always fell in the category of “experimental API” so I’ve never had enough confidence in it to use it for a lot of production components.
The new API, however, takes a new approach to contexts and makes the feature more accessible. It is available in React 16.3.0, you can read a lot more about it and how to use it in [this article]https://medium.com/dailyjs/reacts-%EF%B8%8F-new-context-api-70c9fe01596b). For the purpose of this post, I’ll just keep it short and explain the 3 main items that make up this new pattern:
React.CreateContext
: a function that returns an object with a Provider
and a Consumer
Provider
: a component that accepts a value propConsumer
: a Function as Child component with the value from the Provider
as a parameter
With these new items, we’ll see that it is possible to create a better sub-component pattern that answers all the flaws stated in the first part.For this part, we’ll try to build the same Article
component that we’ve built in my first post, but this time using contexts.
In order to achieve this, we’ll need to create an ArticleContext
. This will give us an ArticleContext.Provider
component that will be our main parent, which we’ll rename Article
, and an ArticleContext.Consumer
, which will help us build all the sub-components we need.
Let’s start this example by implementing a Title
sub-component:
import React from "react";
// This creates the "Article Context" i.e. an object containing a Provider and a Consumer component
const ArticleContext = React.createContext();
// This is the Title sub-component, which is a consumer of the Article Context
const Title = () => {
return (
<ArticleContext.Consumer>
{({ title, subtitle }) => (
<div style={{ textAlign: "center" }}>
<h2>{title}</h2>
<div style={{ color: # ccc" }}>
<h3>{subtitle}</h3>
</div>
</div>
)}
</ArticleContext.Consumer>
);
};
// This is our main Article components, which is a provider of the Article Context
const Article = props => {
return (
<ArticleContext.Provider {...props}>
{props.children}
</ArticleContext.Provider>
);
};
Article.Title = Title;
export default Article;
The example above shows how we can leverage Consumers and Providers to obtain the same sub-component pattern as we had in the first example of my previous article. If you compare this code in the link with the code above, you can see that the latter feels way simpler. Indeed, thanks to the new Context API, there is no need to build and use the findByType
util. Additionally, we don’t rely on the displayName
or name
property of the sub-component to know how to render them.
In the code below, we can see that the resulting Article
component is much easier to use. Instead of passing children to the Title
sub-component, we just need to pass them in the value prop of Article
, which will make them available to every Consumer of the Article context (i.e. to every sub-component defined as a Consumer of this context).
import React, { Component } from "react";
import Article from "./Article";
class App extends Component {
constructor() {
this.state = {
value: {
title: <h1>React sub-components with</h1>,
subtitle: (
<div>Lets make simpler and more flexible React components</div>
),
},
};
}
render() {
const { value } = this.state
return (
<Article value={value}>
{
/* no need to pass any children to the sub-component, you can pass
your render functions directly to the title and subtitle property in
the content object which is passed as value from our context provider
(i.e. Article)*/
}
<Article.Title />
</Article>
);
}
}
export default App;
Moreover, if we want to wrap Article.Title
in another div or component, we can now do that as well. Given that the implementation of the findByType
util in my first post was relying on the direct children of Article
, sub-components were restricted to be direct children and nothing else, which is not the case with this new way of doing sub-components.
Note: You can see above that my value
object passed to the provider is set to the parent’s state. This is to avoid creating a new object for value
all the time which will trigger a re-render of Provider and all of the consumers. See https://reactjs.org/docs/context.html#caveats
Additionally, we can make the piece of code above look even better. By simply exporting the Title
functional component in Article.js
we can give up the <Article.Title/>
notation and simply use instead <Title/>
.
import React, { Component } from "react";
import Article, { Title } from "./Article";
class App extends Component {
constructor() {
this.state = {
value: {
title: <h1>React sub-components with</h1>,
subtitle: (
<div>Lets make simpler and more flexible React components</div>
),
},
};
}
render() {
const { value } = this.state
return (
<Article value={value}>
<Title />
</Article>
);
}
}
export default App;
This is purely aesthetic though, and I personally prefer the first implementation. It gives more context about where a given sub-component comes from and with which parent component it can be used, and also avoids duplicated name issues.
When showing this new pattern to some other developers who were familiar with using the one described in my first article I got one critique: it’s not possible to whitelist children anymore; anything can go within the parent component. While this new implementation is more flexible, the first one was able to restrict the children of a component to only its sub-components. There are multiple ways to fix this, but so far the only one I’ve explored is by using flow. I’ll detail the process in my next article.
In the code snippets below, you will find:
Article
component code and all its sub-components in Article.js
App.js
where you can see how we use the full component and sub-componentsimport React from "react";
// This creates the "Article Context" i.e. an object containing a Provider and a Consumer component
const ArticleContext = React.createContext();
// This is the Title sub-component, which is a consumer of the Article Context
const Title = () => {
return (
<ArticleContext.Consumer>
{({ title, subtitle }) => (
<div style={{ textAlign: "center" }}>
<h2>{title}</h2>
<div style={{ color: # ccc" }}>
<h3>{subtitle}</h3>
</div>
</div>
)}
</ArticleContext.Consumer>
);
};
const Metadata = () => {
return (
<ArticleContext.Consumer>
{({ author, date }) => (
<div
style={{
display: "flex",
justifyContent: "space-between"
}}
>
{author}
{date}
</div>
)}
</ArticleContext.Consumer>
);
};
const Content = () => {
return (
<ArticleContext.Consumer>
{({ content }) => (
<div style={{ width: "500px", margin: "0 auto" }}>{content}</div>
)}
</ArticleContext.Consumer>
);
};
// This is our main Article components, which is a provider of the Article Context
const Article = props => {
return (
<ArticleContext.Provider {...props}>
{props.children}
</ArticleContext.Provider>
);
};
Article.Title = Title;
Article.Metadata = Metadata;
Article.Content = Content;
export default Article;
import React, { Component } from "react";
import Article from "./Article";
const text = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
`;
class App extends Component {
constructor() {
this.state = {
value: {
title: <h1>React sub-components with</h1>,
subtitle: (
<div>Lets make simpler and more flexible React components</div>
),
author: "Maxime Heckel",
date: <i>April 2018</i>,
content: <p>{text}</p>,
},
};
}
render() {
const { value } = this.state
return (
<Article value={value}>
<Article.Title />
{/* here we can see that wrapping the metadata sub-component is now possible thanks to contexts*/}
<div style={{ width: "500px", margin: "80px auto" }}>
<Article.Metadata />
</div>
<Article.Content />
</Article>
);
}
}
export default App;
If you feel like playing with this pattern I’ve made the example of this article available on Github here, you can set up the dockerized project using docker-compose build && docker-compose up
, or just run yarn && yarn start
if you want to run it directly on your machine.
If you liked this article don’t forget to hit the “rocket” button and if you have any other questions I’m either reachable on Twitter, or from my website.
Maxime
Maxime Heckel
Engineer @Docker. 20 something French coffee addict new to the Bay Area. Running code, roads and climbing walls.
See other articles by Maxime
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!