This site runs best with JavaScript enabled.

Rendering a function with React

November 13, 2017

Video Blogger

Photo by paul morris on Unsplash


How I tricked React into rendering a function (not call it... render it)

EDIT: This "feature" will soon be 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:

1import getContent, {init} from 'react-i18n'
2init({
3 content: {
4 pages: {
5 home: {
6 nav: {
7 about: 'About',
8 contactUs: 'Contact us',
9 },
10 },
11 },
12 },
13})
14
15// here's the sorta-curried part...
16// These all result in exactly the same thing: "About"
17getContent('pages.home.nav.about')
18getContent('pages')('home')('nav')('about')
19getContent('pages.home')('nav.about')
20getContent('pages')('home.nav')('about')
21// 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 newsletter...

With React

So thinking about this in the context of React:

1const getHomeContent = getContent('pages.home')
2const ui = (
3 <a href="/about">
4 {getHomeContent('nav.about')}
5 </a>
6)
7// that'll get you:
8<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:

1const ui = (
2 <a href="/about">
3 {getContent('pages.typo.nav.about')}
4 </a>
5)
6// that'll get you:
7<a href="/about">{pages.typo.nav.about}</a>
8// note, that's a string of "{" and "}"...
9// 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:

1const getHomeContent = getContent('pages.typo')
2const ui = <a href="/about">{getHomeContent('nav.about')}</a>
3// 💥 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!

1const getHomeContent = getContent('pages.typo')
2const ui = (
3 <a href="/about">
4 {getHomeContent('nav.about')}
5 </a>
6)
7// that'll get you:
8<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:

1const getHomeContent = getContent('pages.typo')
2const aboutContent = getHomeContent('nav.about')
3const 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.

1> printWarning
2> warning
3> warnOnFunctionType
4> 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

1const ITERATOR_SYMBOL =
2 (typeof Symbol === 'function' && Symbol.iterator) || '@@iterator'
3
4// ...
5
6function iterator() {
7 let timesCalled = 0
8 // useful logging happens here...
9 return {
10 next() {
11 // this is called twice. Once to get the value, and the second time
12 // will report that it's done.
13 return {done: timesCalled++ > 0, value: pathAsString}
14 },
15 }
16}
17
18// ...
19
20contentGetterFn[ITERATOR_SYMBOL] = iterator
21
22// ...

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!

Things to not miss:

Share article