Typescript
hooks
react native
mobile development
React Native lets us build native mobile applications with the declarative simplicity of React. By adding TypeScript, we gain clarity and safety: we know what type of data each component handles, and the editor helps us avoid errors in real time.
In this article, you'll learn three essential pillars of modern React Native development:
useState
Before understanding useState
, we need to imagine how React thinks. Each component is like a small state machine; when those data change, the UI should automatically update.
In a real mobile app, state can represent many things:
In React, these changing values are stored with the useState
hook. This hook lets us remember data between renders and update them reactively, without manually manipulating the UI.
1import React, { useState } from 'react'; 2import { View, Text, Button } from 'react-native'; 3 4export default function Counter(): JSX.Element { 5 const [count, setCount] = useState<number>(0); 6 7 return ( 8 <View style={{ padding: 20 }}> 9 <Text style={{ fontSize: 24 }}>You clicked {count} times</Text> 10 <Button title="Increment" onPress={() => setCount(count + 1)} /> 11 </View> 12 ); 13}
Explanation
useState<number>(0)
creates a state variable count
that can only be a number.setCount
updates the state and triggers an automatic re-render.💡 In Kotlin, this is similar to using a MutableStateFlow<Int>
or a LiveData<Int>
observed by the UI.
useEffect
useEffect
is used to run code after the component renders, or when certain values change. It's the equivalent of onCreate
and onDestroy
in Android, or viewDidLoad
and viewDidDisappear
in iOS, but expressed declaratively.
1import React, { useEffect } from 'react'; 2import { View, Text } from 'react-native'; 3 4export default function SimpleEffect(): JSX.Element { 5 useEffect(() => { 6 console.log('🟢 Component mounted'); 7 8 return () => { 9 console.log('🔴 Component unmounted'); 10 }; 11 }, []); 12 13 return ( 14 <View style={{ padding: 20 }}> 15 <Text style={{ fontSize: 20 }}>Check the console</Text> 16 </View> 17 ); 18}
What happens
return
) runs.Conceptual equivalents:
Android | React Native | Swift |
---|---|---|
onCreate() | useEffect(() => {...}, []) | viewDidLoad() |
onDestroy() | return () => {...} | viewDidDisappear() |
1import React, { useState, useEffect } from 'react'; 2import { View, Text } from 'react-native'; 3 4export default function Timer(): JSX.Element { 5 const [seconds, setSeconds] = useState<number>(0); 6 7 useEffect(() => { 8 console.log('⏱ Timer started'); 9 10 const interval = setInterval(() => { 11 setSeconds((prev) => prev + 1); 12 }, 1000); 13 14 return () => { 15 console.log('🧹 Timer stopped'); 16 clearInterval(interval); 17 }; 18 }, []); 19 20 return ( 21 <View style={{ padding: 20 }}> 22 <Text style={{ fontSize: 22 }}>Seconds: {seconds}</Text> 23 </View> 24 ); 25}
In Kotlin:
1override fun onCreate(savedInstanceState: Bundle?) { 2 timer = Timer() 3 timer.scheduleAtFixedRate(object : TimerTask() { 4 override fun run() { 5 seconds++ 6 } 7 }, 0, 1000) 8} 9 10override fun onDestroy() { 11 timer.cancel() 12}
And in Swift:
1override func viewDidLoad() { 2 timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in 3 seconds += 1 4 } 5} 6 7override func viewDidDisappear(_ animated: Bool) { 8 timer.invalidate() 9}
🪄 In React Native you do this with a single structure, more declarative and predictable.
1import React, { useState, useEffect } from 'react'; 2import { View, Text, Button } from 'react-native'; 3 4export default function DependentEffect(): JSX.Element { 5 const [count, setCount] = useState<number>(0); 6 7 useEffect(() => { 8 console.log(`Counter changed to: ${count}`); 9 }, [count]); // only runs when count changes 10 11 return ( 12 <View style={{ padding: 20 }}> 13 <Text style={{ fontSize: 20 }}>Counter: {count}</Text> 14 <Button title="Increment" onPress={() => setCount(count + 1)} /> 15 </View> 16 ); 17}
💡 In Kotlin, you could compare this to observing a LiveData<Int>
, and in Swift to a property didSet
.
Conditional rendering lets you show different views based on state.
1import React, { useState } from 'react'; 2import { View, Text, Button } from 'react-native'; 3 4export default function LoginExample(): JSX.Element { 5 const [loggedIn, setLoggedIn] = useState<boolean>(false); 6 7 return ( 8 <View style={{ padding: 20 }}> 9 {loggedIn ? ( 10 <> 11 <Text style={{ fontSize: 20 }}>Welcome back!</Text> 12 <Button title="Log out" onPress={() => setLoggedIn(false)} /> 13 </> 14 ) : ( 15 <> 16 <Text style={{ fontSize: 20 }}>Please log in</Text> 17 <Button title="Log in" onPress={() => setLoggedIn(true)} /> 18 </> 19 )} 20 </View> 21 ); 22}
This is similar to using an if
or when
in Kotlin, but inside JSX itself, mixing logic and presentation.
To render lists in React Native, use FlatList
, optimized for performance.
1import React from 'react'; 2import { View, Text, FlatList } from 'react-native'; 3 4interface Task { 5 id: string; 6 text: string; 7} 8 9export default function TodoList(): JSX.Element { 10 const tasks: Task[] = [ 11 { id: '1', text: 'Learn useState' }, 12 { id: '2', text: 'Practice useEffect' }, 13 { id: '3', text: 'Master conditional rendering' }, 14 ]; 15 16 return ( 17 <View style={{ padding: 20 }}> 18 <FlatList 19 data={tasks} 20 keyExtractor={(item) => item.id} 21 renderItem={({ item }) => ( 22 <Text style={{ fontSize: 18, marginVertical: 4 }}>• {item.text}</Text> 23 )} 24 /> 25 </View> 26 ); 27}
Concept | Kotlin | Swift / SwiftUI | React Native |
---|---|---|---|
State | LiveData , MutableStateFlow | @State , @Published | useState |
Lifecycle effects | onCreate() , onDestroy() | .onAppear {} , .onDisappear {} | useEffect |
Conditional view | if , when | if , switch | {condition ? <A/> : <B/>} |
Dynamic lists | RecyclerView | List | FlatList |
React Native with TypeScript teaches you to think in states and effects, not instructions. The data flow is unidirectional, and components automatically react to changes, without you having to “tell them what to do”.