This blog post is archived. It's no longer maintained and may contain outdated information.

Rendering a function with React

November 13th, 2017 — 6 min read

by paul morris
by paul morris
No translations available.Add translation

This "feature" was removed from React 16, so please don't rely on it. That said, this is kinda fun so keep reading!

This week I was working on an internal module at PayPal and had to do something kinda sorta-hacky with React. I thought You'd be interested to hear what I learned.

No, this isn't about render props. If you were hoping for that... callback later 😉

I see what you did there

Context

So react-i18n (not the npm one... one we made at PayPal internally) has this API that I call "sorta-curried". I wrote about it a bit in my last newsletter.

So here's an example:

import getContent, { init } from 'react-i18n'
init({
	content: {
		pages: {
			home: {
				nav: {
					about: 'About',
					contactUs: 'Contact us',
				},
			},
		},
	},
})

// here's the sorta-curried part...
// These all result in exactly the same thing: "About"
getContent('pages.home.nav.about')
getContent('pages')('home')('nav')('about')
getContent('pages.home')('nav.about')
getContent('pages')('home.nav')('about')
// etc...

There are reasons the API is this way, and I'm not going to go over them all. If you're not a fan of the API, you're not alone_. But there are reasons for the API as it is and that's not what we're going over in this post... In retrospect, it's a bad API and I shouldn't have done it this way 😅

With React

So thinking about this in the context of React:

const getHomeContent = getContent('pages.home')
const ui = (
  <a href="/about">
    {getHomeContent('nav.about')}
  </a>
)
// that'll get you:
<a href="/about">About</a>

So far so good. But, what if you mess up and have a typo?

Before this week, here's what happened:

const ui = (
  <a href="/about">
    {getContent('pages.typo.nav.about')}
  </a>
)
// that'll get you:
<a href="/about">{pages.typo.nav.about}</a>
// note, that's a string of "{" and "}"...
// not jsx interpolation...

The problem

So that's fine. But here's where things get tricky. Because we return a string of {full.path.to.content}, if content is missing or there's a typo you can't call a function on what you get back. If you try, you're calling a function on a string and that'll give you an error that would crash the app. Error boundaries could help with this, though sometimes we call getContent outside of a React context, so that wouldn't help in every case. Anyway, this will break the app:

const getHomeContent = getContent('pages.typo')
const ui = <a href="/about">{getHomeContent('nav.about')}</a>
// 💥 error 💥

Again, this is happening because getContent('pages.typo') will return the string {pages.typo} (to indicate that there's no content at that path and the developer needs to fix that problem to get the content). The issue is that you can't invoke a string but that's what's happening because getHomeContent is a string, not a function.

A solution and a new problem

So the change I made this week makes it so when there's no content at a given path, instead of a string, it returns a "sorta-curried" function (just like it would if you hadn't made the typo). This way you can keep calling it all day long if you want. No problem.

So now this wont throw an error, but we lose rendering the path if there's no content!

const getHomeContent = getContent('pages.typo')
const ui = (
  <a href="/about">
    {getHomeContent('nav.about')}
  </a>
)
// that'll get you:
<a href="/about"></a>

And we want to make sure that we show the missing content so it's more obvious for developers (yes we log to the console as well) and if the world is on fire 🔥🌎🔥🌏🔥🌍🔥 and the content failed to load for some reason, it's better for a button to say {pages.transfer.sendMoney} than to say nothing at all.

So here's where the challenge comes in. Let's rewrite the above to make this more clear:

const getHomeContent = getContent('pages.typo')
const aboutContent = getHomeContent('nav.about')
const ui = <a href="/about">{aboutContent}</a>

aboutContent in this example is a function because the call to getContent had a typo, so we'll never actually find content that matches the full path. So the challenge is how do we make sure that we can render the full path in a situation like this?

Developing the solution

At first I thought I could monkey-patch toString on the content getter function. But that didn't work. I still got this warning from React:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.

So I stuck a breakpoint at the line where that error was logged and stepped up the stack to find where the problem was.

> printWarning
> warning
> warnOnFunctionType
> reconcileChildFibers <-- ding ding! 🔔

The reconcileChildFibers function is where I discovered that react will check the children you're trying to render to make sure they're render-able.

Looking through that code, it checks if it's an object first, then it checks if it's a string or number, then an array, then an iterator. If it's none of those things, then it'll throw (for a non-ReactElement object) or warn (for a function, like in our case).

So, in my case, the thing I want to render has to be a function due to the constraints mentioned earlier. So I can't make it work as an object, string, number, or array. But I realized that there's nothing stopping me from making my function iterable (if you're unfamiliar, here's the iterators part of my ES6 workshop recording).

So... I made my function iterable 😉

easy button

const ITERATOR_SYMBOL =
	(typeof Symbol === 'function' && Symbol.iterator) || '@@iterator'

// ...

function iterator() {
	let timesCalled = 0
	// useful logging happens here...
	return {
		next() {
			// this is called twice. Once to get the value, and the second time
			// will report that it's done.
			return { done: timesCalled++ > 0, value: pathAsString }
		},
	}
}

// ...

contentGetterFn[ITERATOR_SYMBOL] = iterator

// ...

I made a handy function for this and created a CodeSandbox demo for you to try out! Enjoy!

You're welcome

The cool thing about this too is that I can log an error with a bunch of context to help the developer figure out what's going on. This is possible because if iterator is called I can assume that React is attempting to render the contentGetterFn.

So yeah, there's my use case for making a function iterable 😉

I hope that's helpful and interesting! Good luck!

Epic React

Get Really Good at React

Illustration of a Rocket
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

Want to learn more?

Join Kent in a live workshop

If you found this article helpful.

You will love these ones as well.