When building a solution with use cases that span desktop and mobile, React is a go-to choice. It can be used to create responsive web apps that render beautifully across form factors. Certain projects, however, call for a more native app solution on mobile. React Native is one of our favorite ways to build apps, and it offers some unique advantages when paired with a React web app with overlapping functionality.
Some pieces of an app can easily be shared, like libraries or Redux action types and reducers. Basically any file with a dependency on pure React and Javascript can be imported on both sides.

One of the areas where I really wanted to get reuse was components. React and React Native use a different set of built-in UI components, so in general UI does not transfer cleanly. Not every component renders their own UI however, sometimes they just render children.

Enter the data component. I use this term to refer to a component which knows what data its children need and how to get it, so that they can just worry about rendering it visually. I will lay out a simple example of how these shared components can be inserted into the render tree to quicken development and keep two solutions in sync. By writing data components as you go on one platform, you then have a strong base to build your UI components on when you implement the other.

git submodule add <git clone URL> shared

 

For more information on git submodules, see the official documentation.

Data Components

For this example we will have a single screen which renders two pieces of information: a string identifying the current platform, and, after a delay, a paragraph of Lorem Ipsum fetched from an API. I am using Bacon Ipsum since their API has CORS enabled which will come in handy on web.

Here is the straightforward version without data components in React Native:

export default class LoremIpsumScreen extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: 'Loading...'
    };
  }

  async componentDidMount() {
    await delay(3000);
    let newTextResponse = await fetch('https://baconipsum.com/api/?type=meat-and-filler&sentences=3&format=text&start-with-lorem=1');
    let newText = await newTextResponse.text();
    this.setState({ text: newText });
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.header}>MOBILE VERSION</Text>
        <Text style={styles.text}>{this.state.text}</Text>
      </View>
    );
  }
}

Styling definitions omitted. Arbitrary delay is used for demonstration purposes. Full code available Aquí

Abstracting Data Interactions

Looking at the implementation of LoremIpsumScreen, the only piece that could not be used directly in React on web is the JSX. We can do some refactoring to move the platform agnostic pieces into a new component called LoremIpsumDataand place it in shared/DataComponents.js.

export class LoremIpsumData extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: 'Loading...'
    };
  }

  async componentDidMount() {
    await delay(3000);
    let newTextResponse = await fetch('https://baconipsum.com/api/?type=meat-and-filler&sentences=3&format=text&start-with-lorem=1');
    let newText = await newTextResponse.text();
    this.setState({ text: newText });
  }

  render() {
    return this.props.render({text : this.state.text});
  }
}

 

The key here is the use of render props seen with the call to this.props.render. From React’s documentation:

A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.

Now we have a component that can fetch and manage data, and will accept any UI passed into it!

We add one helper function to shared/DataComponents.js while we’re here. This allows us to easily define a render prop function that can pass args through to children as props. We’ll see it in action in a moment.

export function renderPlatformUI(PlatformChild) {
  return (childProps) => {
    return (<PlatformChild {...childProps} />);
  }
}

 

Back in our React Native code, we can now write a platform specific UI component that can rely on receiving the text it should display through props (this.props.text).

class LoremIpsumUI extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.header}>MOBILE VERSION</Text>
        <Text style={styles.text}>{this.props.text}</Text>
      </View>
    );
  }
}

 

We add a little boilerplate to connect the data component with this UI component, using the helper we saw before to generate the render prop function with our LoremIpsumUI.

import { renderPlatformUI, LoremIpsumData } from './shared/DataComponents.js';

export default class LoremIpsumScreen extends Component {
  render() {
    return (
      <LoremIpsumData render={renderPlatformUI(LoremIpsumUI)} />
    );
  }
}

With that, our React Native implementation is complete and data management is abstracted to a component ready to be used in our React web app as well.

Full code available Aquí

 

Reuse on Web

First, push up your submodule from the React Native workspace and pull the updates in your React workspace. Now all we have to do is write a new platform specific LoremIpsumUI component, and hook it up to the data component we already defined.

import React, { Component } from 'react';
    import './App.css';
    import { renderPlatformUI, LoremIpsumData } from './shared/DataComponents.js';
    
    export default function App() {
    	return <loremipsumdata render="{renderPlatformUI(LoremIpsumUI)}" />;
    }
    
    class LoremIpsumUI extends Component {
    	render() {
    		return (
    			<div classname="App">
    				<header classname="App-header">
    					<h1>WEB VERSION</h1>
    					<p>{this.props.text}</p>
    				</header>
    			</div>
    		);
    	}
    }

 

And there we go, a functional React web app version of our React Native app written with zero additional data handling.

Full code available Aquí

This use case is simple, but it is a glimpse into a powerful design pattern for components. Data components could, and probably will, deal with significantly more complex interactions like subscribing to state updates if you are using Redux.