Learn how to ship impactful customer journeys with Builder

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

react native

Implementing CSS Style Inheritance in React Native

January 2, 2023

Written By Sami Jaber

Sami Jaber is a Software Engineer at Builder.io, and the tech lead on the SDKs.

Recently, one of our users expanded their usage of Builder from their React app to their React Native app. They soon reported a rather troublesome bug: applying text styles to a button’s text did not properly work in React Native.

In this piece, we’re going to dive into this bug, and how the solution involves re-creating some of CSS’s cascading mechanisms for React Native.

The issue

Let’s assume our user is trying to render a button with a blue background and white text that says “Click Me!”.

In the React SDK, the Button component looks like this:

export default function Button(props) {
  return <button {...props.attributes}>{props.text}</button>;
}

Given the appropriate props, this final HTML will look something like:

<button style="color: 'white'; background-color: 'blue'">Click Me!</button>

PS: styles actually end up in CSS classes and not as inlined `style` attributes. I am writing them as such only to simplify the code examples.

In React Native, all text context must be wrapped in a Text component (kind of like a required span). Additionally, you must use View to represent layout elements (think of it as the div of React Native). So the same button looks something like:

import { View, Text } from 'react-native';

export default function Button(props) {
  return (
    <View {...props.attributes}>
      <Text>{props.text}</Text>
    </View>
  );
}

Which results in the following React Native output:

<View style={{ color: 'white', backgroundColor: 'blue' }}>
  <Text>Click Me!</Text>
</View>

The issue here is that the styles are all applied to the parent View. This wouldn’t be an issue on the web, but it is in React Native. Why? Because in React Native, elements do not inherit styles from their parents!

We could manually fix this particular Button component by selectively applying text styles to the inner Text component. But we want a generalizable solution: Builder allows users to drag’n’drop their own custom components into the Builder blocks, and vice versa. Additionally, if a parent block had text styles applied, those still wouldn’t cascade down to this Text component.

While the React Native team may have very good reasons to not implement this core CSS feature, it is something that we certainly need in the Builder.io SDKs, or else our users would have to manually style every single Text block manually. We want the React Native SDK experience to be as close as possible to that of our other web-based SDKs.

To implement CSS inheritance, we need to first make sure we understand it. While MDN has a great in-depth article on CSS inheritance, here’s a brief explanation:

At a high level, CSS inheritance works by climbing up the DOM tree until you find a parent node that provides a value for it. This of course also includes any values set by the node iself.

Things to note:

  • CSS inheritance only applies to certain styles, not all of them.
  • we will ignore !important (for now)

Before explaining our solution, it’s important to briefly explain how the Builder.io SDK works. Its architecture will dictate the solution we choose to implement.

The Builder.io SDK exports a <RenderContent> component. The user will make an API call to Builder, fetch a JSON object that represents their content, and provide it to <RenderContent>. This component is then responsible for rendering the JSON content. Here’s an example of the JSON:

{
  "url": "/",
  "title": "Home",
  "blocks": [
    {
      "@type": "@builder.io/sdk:Element",
      "children": [
        {
          "@type": "@builder.io/sdk:Element",
          "component": {
            "name": "Text",
            "options": { "text": "Welcome to my homepage" }
          }
        },
        {
          "@type": "@builder.io/sdk:Element",
          "children": [
            {
              "@type": "@builder.io/sdk:Element",
              "component": {
                "name": "Button",
                "options": { "text": "About Page", "link": "/about" }
              },
              "styles": { "color": "white" }
            },
            {
              "@type": "@builder.io/sdk:Element",
              "component": {
                "name": "Button",
                "options": { "text": "Contact Us", "link": "/contact" }
              }
            }
          ]
        }
      ],
      "styles": { "font-size": "15px" }
    }
  ]
}

which would render HTML like this:

<div style="font-size: 15px;">
  <div>
    <span>Welcome to my homepage</span>
  </div>
  <div>
    <button style="color: white;">About Page</button>
    <button>Contact Us</button>
  </div>
</div>

Internally, <RenderContent> will loop over the content.children arrays and call <RenderBlock> for each item, until all of the content is rendered.

Given that we are traversing the JSON data top-down from the root down to the leaf nodes, how would we go about implementing style inheritance in React Native?

We decided to implement it in the following way:

  1. write logic to extract inheritable text styles from styles object
  2. store inheritable text styles in a inheritedTextStyles React.Context
  3. merge new styles into this context whenever a node deeper in the tree updates some of those values
  4. use a React.Context value to make the value available in leaf components
  5. consume that context in wrapped Text component

Let’s get to work!

First, we need to grab all inheritable styles from the styles JSON object, we receive from the Builder API. The API guarantees that these styles all map nicely to React Native (it, therefore, excludes things like CSS functions, and special units e.g. vw, vh, etc.).

Let’s implement extractInheritedTextStyles, which returns the subset of styles that we plan on passing down:

const TEXT_STYLE_KEYS = [
  'color',
  'whiteSpace',
  'direction',
  'hyphens',
  'overflowWrap',
];

/**
 * Check if the key represent a CSS style property that applies to text
 * See MDN docs for refrence of what properties apply to text.
 * https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/Fundamentals#summary
 */
const isTextStyle = (key: string) => {
  return (
    TEXT_STYLE_KEYS.includes(key) ||
    key.startsWith('font') ||
    key.startsWith('text') ||
    key.startsWith('letter') ||
    key.startsWith('line') ||
    key.startsWith('word') ||
    key.startsWith('writing')
  );
};

/**
 * Extract inherited text styles that apply to text from a style object.
 */
export const extractInheritedTextStyles = (
  styles: Partial<CSSStyleDeclaration>
) => {
  const textStyles: Partial<CSSStyleDeclaration> = {};
  Object.entries(styles).forEach(([key, value]) => {
    if (isTextStyle(key)) {
      textStyles[key] = value;
    }
  });

  return textStyles;
};

An empty default context will do the trick here:

// stylesContext.js
export default React.createContext({});

Now that we have the context and the extraction logic, we can pass styles down in the recursive RenderBlock calls. We also have to merge the new inherited text styles into the previous context passed from above. Here’s what that looks like:

import StylesContext from './stylesContext';

function RenderBlock(props) {
  const stylesContext = React.useContext(StylesContext);
  // ...the rest of `RenderBlock` code

  return (
    <StylesContext.Provider value={{
      ...stylesContext,
      extractInheritedTextStyles(props.content.styles)
    }}>
      {/* ...The rest of `RenderBlock` render code */}
      {props.content.children.map(childElements =>
        <RenderBlock key={childElements.id} content={childElements} />
      )}
    </StylesContext.Provider>
  )
}

The last piece of this puzzle is to implement a component that wraps Text, and consumes these inherited text styles:

import { Text } from 'react-native';
import StylesContext from './stylesContext';

function BaseText({ style, ...otherProps }) {
  const stylesContext = React.useContext(StylesContext);
  return <Text {......otherProps} style={{ ...stylesContext, ...style }} />
}

And render this inside of our Button (and any other block that renders text):

import { View, Text } from 'react-native';
import { BaseText } from './BaseText';

export default function Button(props) {
  return (
    <View {...props.attributes}>
      <BaseText>{props.text}</BaseText>
    </View>
  );
}

And that’s it! Now, whenever we provide text styles that ought to be inherited, they are going to be stored in this styles context that makes its way down to BaseText.

Do you remember how I mentioned that Builder customers can render their own React Native components inside of Builder content? They can also make sure the <Text> within those components is styled just like the text in Builder by importing the <BaseText> component and using it in their own code! Since the component uses a React.Context to consume the styles, there is no additional work needed on the end-user’s part.

Implementing !important requires a bit more complexity, but is certainly doable. We’ll need to improve our logic to:

  • store whether a value is marked as !important or not:
export const extractInheritedTextStyles = (
  styles: Partial<CSSStyleDeclaration>
) => {
  const textStyles: Partial<CSSStyleDeclaration> = {};
  Object.entries(styles).forEach(([key, value]) => {
    if (isTextStyle(key)) {
      const isImportant = value.endsWith(' !important');

      textStyles[key] = {
        // strip `!important` if it exists
        value: value.replace(/ !important$/, ''),
        isImportant,
      };
    }
  });

  return textStyles;
};
  • make sure not to override an !important value, unless we’re overriding it with another !important value:
function mergeInheritedStyles = (oldStyles, newStyles) => {
  const inheritedTextStyles = { ...oldStyles };

  Object.entries(newStyles).forEach(([key, style]) => {
    // if the parent has an `!important` value for this style, and the current node's value is not `!important`,
    // then we should ignore it.
    if (inheritedTextStyles[key]?.isImportant && !style.isImportant) {
      // TO-DO can we use return? or break?
      return;
    }
  })
}

import StylesContext from './stylesContext';

function RenderBlock(props) {
  const stylesContext = React.useContext(StylesContext);
  // ...the rest of `RenderBlock` code

  return (
    <StylesContext.Provider value={mergeInheritedStyles({
      ...stylesContext,
      extractInheritedTextStyles(props.content.styles)
    })}>
      {/* ...The rest of `RenderBlock` render code */}
      {props.content.children.map(childElements =>
        <RenderBlock key={childElements.id} content={childElements} />
      )}
    </StylesContext.Provider>
  )
}

And finally, we have to map over the object and only grab the value properties

import StylesContext from './stylesContext';

const getInheritedStyleValues = (inheritedTextStyles) => {
  values = {}
  Object.entries(inheritedTextStyles).forEach(([key, style]) => {
    values[key] = style.value
  })
}

function BaseText({ style, ...otherProps }) {
  const stylesContext = React.useContext(StylesContext);
  return <Text {......otherProps} style={{ ...getInheritedStyleValues(stylesContext), ...style }} />
}

We should now have the ability to parse and process !important styles in React Native as well (not that anyone would ever want to do that 😉)

Do you have any suggestions on how we can improve this solution? Is there anything we missed? Please share with us on Twitter!

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI8 MIN
How to Build Reliable AI Tools
November 15, 2024
Web Design11 MIN
Design Smarter with Figma Auto Layout
November 13, 2024
Web Development10 MIN
A Guide to Server-Side Rendering
November 12, 2024