How to Structure Your React Native Application

… 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

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.

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
}
})

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
src
|-- components
|-- MyComponent
|-- hooks
| -- index.js
| -- useData.js
| -- useLayout.js
|-- index.js
// ./src/components/MyComponent/hooks/index.jsexport {default as useData} from './useData'
export {default as useLayout} from './useLayout'
// ./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
// ./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
}
})

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.

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store