How to Structure Your React Native Application

Jascha Kanngießer
6 min readJan 31, 2021

… or what I learned during one year of React Native application development for iOS and Android concerning function components, the beauty of custom hooks, clean code, separation of concerns and testing.

Photo by Jacob Morch on Unsplash

Since almost one year I am working on a project which intends to make the life of a riders (yes, horse back riders 🐎 ) easier and enjoyable. It all started with a website those guys use to sign up for competitions which is quite hard to use on mobile devices. We think there’s lots of potential for improving the mobile experience for endusers and dived into thinking about a good way to provide the content on a mobile-first basis and started to wrap our heads around how this can be achieved using React Native.

Over the course of the last twelve months I noted down many learnings which I’d like to share with you so that you can (hopefully 🤞) benefit from my learnings, too, or can share even brighter ideas how to build a React Native app the right way. Without further do, let’s go!

Structuring things the right way

Structuring your code in a way which is following few well-known principles boosts your development performance and ensures a resilient app shipped to your users. Applying principles like DRY, separation of concerns, KISS, Open-Closed principle, … (you name them, here’s a nice list not only applying to Java though 💡) helped me developing specific patterns I currently apply all over my app.

The pattern is to ensure that code is separated the kinds of task its intended for: visualising (rendering) content, handling domain (business) logic and handling UI logic such as animations. To ensure for smooth readability, testability and maintainability my main principle is:

Never ever mix any of the aforementioned kinds into a single file, but instead use one file per kind and custom hooks for handling your domain and UI logic.

Let’s start with an example

See this component? It’s taking care of different things like fetching information, handling animations and showing the information using a ScrollView to the user. I added some blanks to make it more readable. 🤓

// ./src/components/MyComponent.jsconst MyComponent = () => {  
const scrollY = new Animated.Value(0)
const height = new Animated.Value(0)
const opacity = scrollY.interpolate({
inputRange: [0, 45],
outputRange: [0, 1]
})
const {dark} = useTheme() const [info, setInfo] = useState({loading: true, info: undefined})

useEffect(() => {
(async () => {
// Fetch information on mount
const result = await fetchSomeData()
setInfo({loading: false, info: result})
})()
}, [])
return info.loading ? <Loading /> : (
<>
<Animated.View
style={[styles.header, {opacity, height}]}>
<BlurView
blurType={dark ? 'dark' : 'light'}
blurRadius={100}
style={styles.blur} />

<View
onLayout={Animated.event(
[
{
nativeEvent: {layout: {height}}
}
],
{ useNativeDriver: false },
)}
style={[
styles.headline,
{paddingTop: useSafeAreaInsets().top},
]}>

<Header>
My Header above blurred background
</Header>
</View>
</Animated.View>
<Animated.ScrollView
style={[
styles.container,
{paddingTop: useSafeAreaInsets().top}
]}
showsVerticalScrollIndicator={false}
contentInsetAdjustmentBehavior='automatic'
scrollEventThrottle={1}
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: scrollY
}
}
}
],
{ useNativeDriver: false }
)}>
<Some Content info={info.result} />
</Animated.ScrollView>
</>
)
}
const styles = StyleSheet.create({
header: {
...StyleSheet.absoluteFillObject,
bottom: undefined,
elevation: 1,
zIndex: 1
},
blur: {
flex: 1
}
})

You can see that the component is pretty much cluttered with different tasks like fetching the content using the useEffect hook, rendering either the <Loading /> component or fetched content and handling some scroll animations like showing a blurred background behind the header title once the user starts scrolling down. It also handles some state using useState and handles some visualisation logic. I see this “mess” (sorry! 🥸) in many components on the web and always pray to god it’s only for the sake of writing small examples (just kidding, it’s not that bad though — but even when writing examples I feel like it should be good practice to show others how to write good code).

If you now wanted to go ahead and test this component, you’d end up with a very messy and long test file, mocking out different components and libraries, defining test cases not really related to each other since some are comparing the generated snapshots, others test the async fetch logic and again others make sure the animations work the right way.

Refactoring

As postulated in the very beginning, separating the different concerns in different files and making use of patterns like custom hooks in React Native helps us a great deal in making our app more maintainable. Right now, our app (folder) structure looks like this:

src
|-- components
|-- MyComponent.js

I go ahead and move the file MyComponent.js into it’s own folder and rename it to index.js. By creating a folder also named MyComponent and creating an index.js file inside it containing the code, other components referencing MyComponent aren’t affected by these changes. Also I add a hooks folder in that newly created folder which I’ll move all the logic to. As long as MyComponent is the only component requiring the domain logic to fetch the data stored in useData.js, I’ll keep it there. If there would be other components relying on the same logic, that specific hook would move up the tree accordingly.

src
|-- components
|-- MyComponent
|-- hooks
| -- index.js
| -- useData.js
| -- useLayout.js
|-- index.js

The hooks/index.js file looks like this:

// ./src/components/MyComponent/hooks/index.jsexport {default as useData} from './useData'
export {default as useLayout} from './useLayout'

The useData hook now contains all the logic for fetching the data and exporting the loading flag as well as the result returned from the server.

// ./src/components/MyComponent/hooks/useData.jsconst useData = () => {  
const [info, setInfo] = useState({loading: true, info: undefined})

useEffect(() => {
(async () => {
// Fetch information on mount
const result = await fetchSomeData()
setInfo({loading: false, info: result})
})()
}, [])

return info
}
// Split default exporting and hook declaration to avoid anonymous // symbols in logs and dumps
export default useData

Following this pattern the useLayout hook contains the following code:

// ./src/components/MyComponent/hooks/useLayout.jsconst useLayout = () => {  
const scrollY = new Animated.Value(0)
const height = new Animated.Value(0)
const opacity = scrollY.interpolate({
inputRange: [0, 45],
outputRange: [0, 1]
})
const {dark} = useTheme() const onLayout = Animated.event(
[
{
nativeEvent: {layout: {height}}
}
],
{useNativeDriver: false}
)
const onScroll = Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: scrollY
}
}
}
],
{useNativeDriver: false}
)

return {
blurType: dark ? 'dark' : 'light',
opacity,
height,
onLayout,
paddingTop: useSafeAreaInsets().top,
onScroll
}
}
export default useLayout

Putting things together

Refactoring the animation and data loading logic into separate hooks our MyComponent, now moved into the new index.js file, looks like this:

// ./src/components/MyComponent/index.jsimport {useData, useLayout} from './hooks'const MyComponent = () => {  
const {
blurType,
opacity,
height,
onLayout,
paddingTop
} = useLayout()
const {loading, result} = useData() return loading ? <Loading /> : (
<>
<Animated.View style={[styles.header, {opacity, height}]}>
<BlurView
blurType={blurType}
blurRadius={100}
style={styles.blur} />
<View
onLayout={onLayout}
style={[styles.headline, {paddingTop}]}>
<Header>
My Header above blurred background
</Header>
</View>
</Animated.View>
<Animated.ScrollView
style={[styles.container, {paddingTop}]}
showsVerticalScrollIndicator={false}
contentInsetAdjustmentBehavior='automatic'
scrollEventThrottle={1}
onScroll={onScroll}>
<Some Content info={result} />
</Animated.ScrollView>
</>
)
}
const styles = StyleSheet.create({
header: {
...StyleSheet.absoluteFillObject,
bottom: undefined,
elevation: 1,
zIndex: 1
},
blur: {
flex: 1
}
})

While not changing any of the logic but only moving it to different files, we made our main component way more readable. At the same time, the logic could be easily re-used in other components as well if required. The testability of our components and (now) hooks improved greatly. We can now easily exchange the visualisation of the data with a different component if needed while not touching our domain logic because we refactored that logic into separate custom hooks, plus all other components using MyComponent wouldn’t have to adjust anything since we moved the component into it’s own folder using a index.js file.

Conclusion

Following this approach helped me maintaining my code coverage at 100% , keeping the individual file size small and tests short to read, while allowing me to develop new components and domain logic fast. However the pattern described above might not always apply, however I try to apply it whenever possible. As of now I couldn’t come up with any example where I couldn’t apply this pattern, thus I hope you anyways follow this style already (and therefore are rightfully bored if you made it till this line 😉) or you found something useful and new in this article.

Anyhow, feel free to leave a comment whether you like this approach or not. Something I can improve? Let me know! Something else you’re interested in concerning React Native and mobile app development? Let me know, too! Enjoy! 🚀

--

--

Jascha Kanngießer

Jascha is a developer by 😻, employed with a big software company and spending some of this spare time on things like mobile app development.