Before you read this, you might want to check out ES6, if you haven’t already. The code should be easy enough to understand, even if you don’t.
If you want to follow along or play with the setup, clone this repository and follow the instructions given in the README.
Let’s start with a simple React component that displays a text box for a user to enter their name :
Here’s what the code for it might look like :
Nothing special, just your average controlled component. Now let’s add some more functionality to this component. Let’s make it do the following while the user is typing :
- Check that the name is no longer than 15 characters.
- Check that the name doesn’t contain the character ‘@’.
- Send the name entered to a server, and show whether the current value is in sync with the value on the server.
When we’re done, we want it to look something like this :
To do this, let’s add a method ‘validate’ that performs the validations and returns a list of errors, and another method ‘syncToServer’ which, for now, mocks a server call using a 2-second timeout. The code for it might look something like this :
Our component is looking pretty good! Wouldn’t be great though if we could use the same component in our React Native app? I’d love to avoid rewriting the same validations and server-syncing logic for the native app. Currently, however, there are two things preventing us from doing this :
- The base UI components in React (DOM) and React Native are different.
- The ‘React’ objects in React and React Native are different (React Native removes the DOM-specific stuff adds a lot of native-specific code).
Both of these issues are likely to be resolved soon, maybe in a few months, maybe sooner. But if you’re impatient like me, don’t worry, there’s a way you can do it today. Let’s define the ‘Name’ component slightly differently :
Woah! What just happened? Where’s the import for React? What’s that RenderView thingy? We made 2 changes to the component :
- Instead of exporting a React component class, we exported a function which takes the ‘React’ object as argument, and returns a class. That’s why we didn’t need to import React. This pattern ensures that our implementation is not tied to the ‘React’ object from either React or React Native.
- Instead of returning a tree of DOM nodes directly, we are delegating the responsibility of rendering the right UI elements to another class ‘RenderView’, which is passed in as the prop ‘view’. This allows us to separate the platform-specific UI, layout and styling from the platform-independent logic.
Before we can use this though, we need to define a view component, which wraps the render method of the original ‘Name’ component :
Now let’s wire it all together to render ‘Name’ on the web application :
Great! It works the same as before. So what did we achieve here? Just some refactoring and a round-about way to do the same thing? Of course not! Let’s define a native view component for ‘Name’ :
Again, this is pretty much the same UI code, translated into native elements, instead of DOM. Let’s put this all together in a simple native app :
Here’s what it looks like on Android (iOS is pretty similar too) :
That’s a 100% native app running 100% shared business logic. And every change you make to Name will be reflected instantly across both web and native apps (with the right setup, of course).
That’s it! That’s all you need to do. This is a simple pattern with no external dependencies, and works great for sharing components between React and React Native apps. Have fun sharing components!
While I have passed in the RenderView component as a prop in the above example, that’s not the only way to do it. Here a few other ways :
- You can pass it in as an argument along with the React object. This makes sense if you are sure that you will always use the same view for a particular shared class.
- You can use a global object which maintains a mapping from the shared classes to platform-specific views.
- Rather that use a global object, you can supply the mapping using the React context if you have a huge tree of elements and want to assign views to them all at once.
- You can pass in the view as a child element, and clone it with the desired props. You can even pass in multiple children with different UIs, and they will all remain in sync in real-time! Try it, it looks pretty cool.
- You can use a combination of one or more of the above, with priorities assigned to each one, so that you can have a set of default views, and override them as required. Beware though, this can get pretty complex pretty quickly.