Yes, React Native is a modern and a very young framework. It was released in June 2015, so it's been available for two years now. At ticketea we started a new app in September 2015, just a few months later the first release of React Native. The framework was ready, but still bare bones. Much has changed since then and also many libraries have appeared during these two years.
We thought that sharing our experience with the development community was the right thing to do, since RN wasn't the happy path we thought it was because we faced some issues. Maybe... Should we have called it ‘React Naive’ back then?
Libraries and dependencies
We will start talking about the bricks that make the project possible.
We embraced global state this time. For any code buddy that has been in this for a while, that usually means The Greater Evil. With Redux you shall not be afraid of it anymore. Yes, the state is global, but you always know what happens in there since only a few files have access to it, and you have to communicate with them via actions (special declarative functions). Sounds a bit overkill at first but it's essentially very simple and elegant.
React Native is, well, reactive too: changes on Redux Store (Global State) have an instant effect in RN components, so it's not like you are dealing with a big singleton (play drama sound here). React Native updates views when its inner state changes, so looks like both tools are made for each other.
However, and this is important, if we wanted to leave the Redux business intact and clean, we could not face any kind of situation where RN could directly modify it. Both counterparts cannot know about each other. So, we kept them separate except from one file: the connector.
Every React component that receives state from Redux (normally screen containers or so called ‘scenes’) has this connector thing that maps Redux elements into React properties, and Redux actions into simple functions. When you execute one of these functions within a React component you're actually dispatching an action to Redux. So Redux receives this action, some reductor captures it, and it returns a new State; then the connector passes down the new data, and React components magically update.
This is the happy path, but how many times do we have to manage asynchrony? If we were to handle async responses from Redux actions we would have to make React components aware of this Redux magic world! Noooo! We cannot dispatch, wait, handle, parse and do things. That'd be dirty. Moreover, a reducer should only be able to mutate a single piece of this Global State. What if we need to dispatch some action that needs to touch two pieces of the State? Imagine this simple (and real) situation: we fetch your tickets and so their related events, stored separately in the store. We must check the tickets, save them, and then download the events for those tickets, and save them too. The easy solution would be to dispatch two actions, right? Be aware that one action depends on the other, so it's not just a matter of creating a promise chain. React components should not have to know wether they need the events information at any given order, nor the knowledge that one information depends on another. Components are dumb, and they follow a predictable and declarative approach. Chaining asynchronous actions inside a view doesn't look like it.
Introducing Redux Saga.
Believe me when I say that I don't believe in silver bullets. I don't think there is a single best solution for software problems (nor will ever be), but Redux Saga is so good it made me question if I was wrong for a second and a half. Then I realised that this library only serves to its Redux master. But marvellously. It's the best tool in the arsenal so far. It's Mr. Wolf.
I'm trying to explain what Redux Saga does now, just for the basics. Redux Saga listens to Redux actions constantly, and when the right action passes through, it calls a custom function of yours, able to perform any kind of stuff and dispatch other actions you want. So an action can only handle one reducer, but since the saga handles ‘n’ reducers, you can funnel through any complex stuff, specially async calls. This custom function is actually a JS iterator that is able to yield actions, which mutate your State as it receives API responses from your server. Clean and elegant.
It's actually more powerful and handles edge cases well, which is important for those tricky features that would became a workaround using other opinionated libraries (in Spain we call that a ñapa).
Before closing on Redux and Redux Saga, we wanted to share a few tips that have proven useful to us in the long run:
Every asynchronous action is made by actually three actions:
The former dispatches the action that triggers the Saga and sets the proper State for the LOADING mode. The other two are sent from the Saga itself for the good end and the bad one. This way we never forget that we must inform the user to wait and that we must handle errors. For those sync actions we normally prefix them with SET.
For every State chunk and reducer, write a list of constants for the action names (
my_namespace/MY_ACTION_NAME_REQUEST) that affect it. One single place to retrieve them will save your from many a headache and will ease your understanding about what's going on.
Write React components as directories (when it makes sense): use a
index.js file to act as an entry point (so you will only have to import the directory and never know about the files within it). Inside this index, if necessary, import the connector and export it. The connector will export the
component.js file that includes the actual React Native code. You may also include a
style.js in order to include spreadsheets, to keep them outside the component itself. Clean as a whistle!
Beware that Redux is somehow an ‘all or nothing’ paradigm. It prevents us from using and mutating state within React components (something that is hard to track). However, apart from inner state, everything else becomes Redux State. Therefore, for basic features, it can become over-engineering, specially working alongside Redux-Saga. For the most simple cases, I would even suggest to follow the same Redux pattern. Just consider if these are not the 70% of your app. Then, avoid Redux.
About React Native
As far as I am concerned, we must work with RN with an almost completely functional (FP) approach. This means that, whenever possible, you must choose the path of the 'pure' component, which means: components with no inner state (and no lifecycle, just a render that prints passed-in props). Avoid ES6 classes if you don't need them. At the end of the day, it could be a small boost on performance too.
this if possible. Embrace referential integrity, if doing so doesn't add complexity on your implementation logic. RN components should be as dumb and static as possible. Make static functions and pass props directly from the render. The less references to inner state and properties, the easier will be to test components (and you will avoid stupid bugs with wrong references to this instances at the same time).
Now we are entering in the controversy world: do we use ImmutableJS? If you don't know what this is (I didn't before I started with this project) I'll put it in plain words: it's a wrapper that encapsules your State so you can only communicate with it via special commands. Why? Because every time you change state, this library handles the best way to mutate it, and it makes a copy of it. If you are into functional programming you know how much important immutability is.
Well, now the question is how deeper do you want this library to dig in? I don't think there is a proper answer to this. Not until you are full of shame and you know you already made the wrong choice and it's too late to go back. Anyway, our decision was to make Immutable working solely on the whole Redux Store. Only Reducers know about it. This decision has some drawbacks:
- Immutable.js must be used even in the most simple reducers.
- Since React Native should not have to deal with Immutable objects, you’ll have to parse them to JS before they get to React Native. And every change in the State has a performance cost...
- Redux Store middleware libraries do not have to be aware of this Immutable objects, so you better find another middleware dependency on top of the usual ones, so you can get free support for Immutable objects.
Have you dealt with this? Tell us your thoughts!
Now, persistence. How do we persist user data from one session to another? On a native app, you look for the usual ORM frameworks, or Realm. We didn’t need a full solution like that. Just the simplest of ways to retrieve some user details. Everything else will be fetched from the network. So, what’s the most common persistent tool in React Native: AsyncStorage. React Native supports its own ‘key-value storage ’ engine, but even Facebook suggests not to handle it with your bare hands. At this time we are using redux-persist wrapper, which has worked pretty well so far. In our case, as previously stated, we deal with ImmutableJS so we use redux-persist-immutable instead. Yes. Yet another dependency from a dependency. ImmutableJS has drawbacks, and this is a big one. Every time we deal with some add-on for Redux Storage, this issue faces up.
On top of this
Do we get anything in exchange? There remains the big question: is React Native worth the effort? Should I rely my business app brand on a framework which is still on a 0.x version? How much do I gain and how much do I lose? Clearly a RN app is not a WIN-WIN. None is.
If you choose the full native way, you’ll have to deal with two separate apps, and minimum two developers to keep the pace for constant updates. On the other hand, you have full control of the app. With ReactNative you lose a lot of control over your app (you are behind a huge layer of abstraction), but instead you get a lot of the job done by making one single app.
Maintenance is hard. Dependencies with even more peer dependencies that are no longer compatible with each other is not something that occurs every week, but it’s an issue critical enough to not lose sight of. That added with an abstraction layer and code that has to support many operating systems and devices for both platforms (Android and iOS). If your job is to ‘make & deliver’, ignoring RN might sound like killing the goose that lays the golden eggs. If you are maintaining a product, be careful: it can bite you back on the long run.
Developer tools are different, but they are great. Sometimes you will be able to use the usual tools from the platform IDE, but most of the time you’ll be using web developer tools. I am a mobile developer and I prefer the language and tools provided by Xcode, for instance. However, it’s been a good developer experience to take advantage of Redux and React dev tools. It’s a matter of getting used to them, I suppose.
This one is obvious: you depend a lot on Apple, Google and Facebook. Take it into account. Everybody makes mistakes. Everyone throws bugs in production code. We suggest to follow all good practices from day one, even if it seems unbearable to learn and slows you down. Following what everybody does will help you detecting and patching issues on your code. By choosing custom workarounds you’ll end up in trouble soon.
Did this help you? Please, let us know in the comments below!