React: How to render object with custom toString?

Created on 20 Apr 2017  ·  15Comments  ·  Source: facebook/react

I'm implementing a i18n library, and would like to render a object with custom toString like this:

const i18nObject = {
  toString() {
    return 'xxx';
  }
};
render() {
  return {i18nObject}
}

But got the error:

invariant.js:44 Uncaught Error: Objects are not valid as a React child (found: xxx). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of App. at invariant (invariant.js:44)

Does anyone know if there's any method to do this?

Question

Most helpful comment

Wow! 3 responses within a minute window. What a helpful community this is! 🙇

Edit @aweary has since removed his response, making it seem like I can't count 😆

All 15 comments

For the time being you'll need to wrap the option in a <span> or similar, eg

render() {
  return <span>{i18nObject.toString()}</span>;
}

In React 16 you'll be able to return the toString() value directly from render though, eg

render() {
  return i18nObject.toString();
}

If there are many i18nObject, then we have to write many time toString.

render() {
   return <div>
        <span>{i18nObject1.toString()}</span>
        <span>{i18nObject2.toString()}</span>
        <span>{i18nObject3.toString()}</span>
   </div>
}

Would like to

render() {
   return <div>
        <span>{i18nObject1}</span>
        <span>{i18nObject2}</span>
        <span>{i18nObject3}</span>
   </div>
}

We won’t be implicitly calling toString(). This would mean allowing arbitrary objects in React tree which will lead to countless app bugs (since it’s very easy to accidentally pass an object instead of a string). We actually supported passing objects (although for keyed fragments rather than printing strings), and it turned out to be confusing and leading to buggy apps.

If you’re concerned about this, you can create a custom I18n component and do something like <I18n>{i18nObject1}</I18n>. The component could then read its children, assert on the output, and call toString():

function I18n(props) {
  // Ensure there's just one child
  const onlyChild = React.Children.only(props.children);
  // Ensure it's a localization object
  if (isI18NObject(onlyChild)) { // implementation up to you
    throw new Error('Expected a localization object as a I18n child.');
  }
  return <span>{onlyChild.toString()}</span>
}

// later
render() {
  return (
    <div>
      <I18n>{i18nObject1}</I18n>
      <I18n>{i18nObject2}</I18n>
      <I18n>{i18nObject3}</I18n>
    </div>
  );
}

Hope this helps!

Unfortunately, in your second example, React will see the incoming values as Objects and assume you intended to pass something else. If you're looking for a slightly shorter syntax you could do something like:

render() {
  return <span>{''+i18nObject}</span>;
}

Anyway, if you're writing an i18n component, maybe you could encapsulate this behavior somewhat so that (1) you don't have to write so many .toString() calls and (2) when you later upgrade to React 16 you can remove the <span> wrappers in a single place?

function i18n(data) {
  return <span>{'' + data}</span>;
  // This can later just become:
  // return data.toString();
}

render() {
  return (
    <div>Hi, {i18n(object)}</div>
  );
}

Yea, this is way simpler than my suggestion 😄

Wow! 3 responses within a minute window. What a helpful community this is! 🙇

Edit @aweary has since removed his response, making it seem like I can't count 😆

I thought I was fast enough to go unnoticed, didn't want to overload @cwtuan with answers 😄

Thanks for everyone!
I'm trying to chain the setter methods for the i18n utility. The method key, values, and defaultMessage will return the same object. But, I don't want to write toString() at the end every time.

render() {
   return <div>
        <span>{i18n.key('k1')}</span>
        <span>{i18n.key('k1').values({v1:123,v2:456})}</span>
        <span>{i18n.key('k1').values({v1:123,v2:456}).defaultMessage('....')}</span>
   </div>
}

@cwtuan as long as the object returned from those methods has the toString method, @bvaughn's or @gaearon's solution should still work

<div>Hi, {i18n(i18n.key('k1').values({v1:123,v2:456}))}</div>

If this was a common pattern you could try building a I18N component that provides an abstraction for calling these setters.

const I18N = ({ source, key, values, defaultMessage }) => {
  let result = source;
  if (key) { result = result.key(key) };
  if (values) { result = result.values(values) };
  if (defaultMessage) { result = result.defaultMessage(defaultMessage) };
 return <span>{result.toString()}</span>
}

This is just a rough implementation to give you an idea of what it might look like.

<I18N
  source={i18n}
  key="k1"
  values={{ v1: 123 }}
  defaultMessage="some default"
/>

...as long as the object returned from those methods has the toString method...

Unfortunately it wouldn't work unless you explicitly called .toString() on it- since it's still an Object type 🙁

👍 for the I18N component suggestion though. Alternately you could go with an overloaded method signature for the i18n function that returned a string type instead of an object

Unfortunately it wouldn't work unless you explicitly called .toString() on it- since it's still an Object type 🙁

What do you mean? If <div>Hi, {i18n(object)}</div> works and object.key(...) returns another object with a toString method, it should continue to work. toString will be implicitly called when you try to concat the object with string.

<div>Hi, {i18n(object)}</div> works okay in my above example b'c i18n returns a string (not an object).

toString will be implicitly called when you try to concat the object with string.

Yes, this is true. But React doesn't concatenate with a string- except for when displaying the invariant error heh. See here.

Hi, {i18n(object)}
works okay in my above example b'c i18n returns a string (not an object).

Right, and what I'm saying is that i18n(object) or i18n(object.key(...).values(...)) will both work since i18n is coercing the object to a string before wrapping it in the span.

Yes, this is true. But React doesn't concatenate with a string- except for when displaying the invariant error heh. See here.

I'm saying i18n concatenates object with a string, which calls toString, not that React will do it internally.

Oh! I didn't notice the edit you'd made 😁 I thought we were still talking about the chained approach (eg i18n.key('k1').values({v1:123,v2:456})). You are correct!

Thanks for all suggestion!

Was this page helpful?
0 / 5 - 0 ratings