Rendering a function with React

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!




Source link

مدونة تقنية تركز على نصائح التدوين ، وتحسين محركات البحث ، ووسائل التواصل الاجتماعي ، وأدوات الهاتف المحمول ، ونصائح الكمبيوتر ، وأدلة إرشادية ونصائح عامة ونصائح