Has it happened to you that after coding the same type of task (creating actions in redux, making new styles, etc.) one hundred times you feel something is off? Something that can be improved in a retrospective session? Well, I feel it a lot! But after the dust settles, I take some time to analyze it myself. What follows is my advice at improving things that you probably do every day in your React Native projects.

Good code abstractions feels like playing tetris with square shapes only

If you are an experienced React Native developer, the ideas in this post might be familiar to you. Maybe you've already solve these problems in other ways. Still, it might be useful to give it a read. If you are a beginner maybe this post can help you make better decisions.

Style file issues

A technique for component building is to have a Typography and Color singleton object.
Let's take a look at these Typography.js and Color.js example files:

Looks fine, but there are a bunch of very common techniques that could be improved:

  • Creating an index.styles.js file near your component
  • Implementing Typography and Color inside index.styles.js with a defined naming that generally is bad (e.g.: title, container, labelButton, submit)
  • Putting paddings/margins inside
  • Importing your style file with import styles from './index.styles.js'

Now take a look at implementation in a common component:

Some problems arising from this are:

  • You can't import index.styles in your component because of how the IDE works
I just commented the import and the IDE can't re-import it
  • Can't jump to definition because of name collision
  • You won't know what it looks like until you run the project
No idea what verticalSpacing means in design terms
  • Your project has a lot of boilerplate files that prevents a better code organization
index.styles everywhere!

You can't remove unused styles easily because you don't know where they are used. You would need another directory because a component now requires it's own index.styles.js so code grow is uncontrolled.

Styles solution

So after a while I noticed we can expand on Typography and Color abstractions:

  • Create a Spacing singleton that contains all required paddings, margins and sizes. This way you can easily see what units are used in the project.
  • Create a Styles singleton that contains all required styles in project. If design rules are well defined, this file would not need to grow that much. Note how it is divided into two parts: helpers (L8-59) and design styles (L61-125).
  • Now implement those styles by composition. This way you know what a component looks like just by it's code. Notice <Labels />, <Link /> and <Actions /> components and how they are related to the design.
Component preview from design file

Looking good right?

The advantages we get from this solution are:

  • Jump to style definition works
  • Auto completion works
  • Increased code readability
  • No index.styles.js files, so there's less code to maintain
  • If a style is misused you can detect it by looking at the code
  • Easier design guidelines to code

Actions issues

Now my complaint about Redux actions. Good practices can't save you from this. An actions file always looks like this:

Repeating const values within constant names is boilerplate. We don't have atoms like in erlang. In that case we could declare a constant, and its value would be the same as the constant name (e.g.: export const ADD_ITEM_TO_LIST;).

Besides that, you need to add action creators for every action with camelCase notation too. Every time a developer wants to know what your app does, they need to follow action creators and actions across the codebase.

And sometimes it's not clear where to put actions and action creators, so readability is hard and developer cognitive resources are scarce.

I dislike not knowing from where an action is dispatched at debug time

Actions solution

So applying OOP from earlier we can achieve a good code reduction:

Now Item, a business entity, is a singleton with a defined set of readable methods. We declare the action type once, and it doesn't matter if it's caps or camelcase, everything after A is just payload info, so we don't need to read it.

From this:

export const FETCH_ITEM = 'FETCH_ITEM'

export const fetchItem = (id: number) => ({
  type: FETCH_ITEM,
  payload: id,
})

to this:

const ItemActions = Object.freeze({
  FetchOneItem: (id: number) => A('ITEMS/FETCH_ONE_ITEM', id),
  ...
})

It can't be easier. Just read from above to below (vertical scanning), identify the action you're looking for, and read the payload section (horizontal scanning); no need to scan large files or scrolling.

The A helper was added to help in debugging. A common approach along most languages is to print the function caller. This would return filename and the line where this action is being dispatched (only when debugging since it's an expensive process). Unfortunately I can't get any stack trace package to work. Another approach is to call f.caller recursively, but it's not a satisfactory solution at the moment. My quest for a good debugging experience shall continue in another post.

What about animations and transitions?

If you work with the Animated library you probably need to duplicate code for fade and/or movement transitions. Because, you know, there's time constraints to finish the product.

Yup, that's a lot of code for a fade, move Y position and rotation...

This can be solved with 1 or 2 hooks for almost all cases, just look:

useRangeFX encapsulates typical transition behavior. It returns an Animated.Value with a toggle function. It only requires 3 arguments: initial, final and duration. So how does our beloved Toast look now?

Best part is useRangeFX can be improved with an options argument (e.g.: {delay, easing, useNativeDriver}) in case you require it. Niceee.

"So what about the useKeyboardHeight hook?" "Why are you sharing it?", you may be thinking. Well, that is used for some components where I need to detect keyboard height (L9, L12):

Just one line and that's it: automatic keyboard handling.

This probably doesn't work across all devices, so maybe you need extra validations (Androooid! >:/ ). But hey! It's just a little price to pay for encapsulation.

Besides, another hook may be required for color blending, but I'll leave that one as a reader exercise.

Conclusion

Code improvement is hard; it requires a lot of practice and time to experiment. That's why retros are suggested between sprints so as a team we can analyze a problem and achieve better results faster.

So far I'm using those improvements in a personal project only - I don't feel confortable experimenting with customer projects, you know 🌝. Maybe with time I can feel confident enough to put a D before that X or maybe there are already better solutions out there.