React Native uygulamalarında Redux ve TypeScript nasıl kullanılır?

Zafer Ayan
6 min readMar 19, 2020

--

TypeScript’in kod kalitesine ne denli katkılar yaptığına dair çokça yazı bulunuyor. Bu yazımda sizlerle Redux ile nasıl geliştirim yapılacağına değineceğim. Öncelikle React Native projemizi typescript şablonu kullanarak oluşturalım ve iOS Simulator’de açalım:

npx react-native init SampleRedux --template react-native-template-typescript
cd SampleRedux
npx react-native run-ios

Uygulamanın başarılı bir şekilde simulator’de çalıştığını gördüysek artık redux’ı yükleyebiliriz:

yarn add redux react-redux redux-logger

Not: react-redux’ı yüklememizin sebebi, react ile ilgili binding’lerin yüklenmesi içindir. react-redux olmadan oluşturulan component’i store.subscribe() fonksiyonu kullanarak store ile haberleştirebiliriz. Fakat bu fonksiyonu kullanarak React Redux’ın sunduğu performans iyileştirmelerinden feragat etmiş oluyoruz. Bunun yerine React Redux’ta yer alanconnect() binding’ini kullanarak kolayca component’i bağlayabiliriz.

Redux’ı TypeScript ile kullanabilmek için gerekli tip belirlemelerini yükleyelim:

yarn add -D @types/react-redux @types/redux-logger

src dizininin içerisinde store adında bir dizin oluşturalım. Bu dizin, redux ile ilgili elemanları barındıracaktır. Daha sonra bu dizin içerisine chat ile ilgili dosyaları barındıracağımız chat dizinini oluşturalım. Aşağıdaki komut ile yapabilirsiniz:

cd src && mkdir store && cd store && mkdir chat && cd ../../

Projedeki dosya hiyerarşimizi aşağıdaki gibi oluşturacağız:

Bunun için öncelikle chat ile ilgili modelimizi barındıracağımız 1_models.ts dosyasını oluşturalım ve içerisine aşağıdaki iki interface’i ekleyelim.

export interface Message {
user: string;
text: string;
timestamp: number;
}
export interface ChatState {
messages: Message[];
}

Message interface’i chat mesajını tutacak, ChatState ise mesaj listesini barındıracaktır. Şimdi, mesajlar ile ilgili aksiyon tiplerinin barındırılacağı 2_actionTypes.ts dosyasını oluşturalım:

import {Message} from './1_models';export const SEND_MESSAGE = 'SEND_MESSAGE';
export const DELETE_MESSAGE = 'DELETE_MESSAGE';
interface SendMessageAction {
type: typeof SEND_MESSAGE;
payload: Message;
}
interface DeleteMessageAction {
type: typeof DELETE_MESSAGE;
meta: {
timestamp: number;
};
}
export type ChatActionTypes = SendMessageAction | DeleteMessageAction;

Mesaj ekleme ve mesaj silme işlemleri için sırasıyla SendMessageAction ve DeleteMessageAction interface’lerini oluşturuyoruz. Daha sonra bu iki tipi pipe “|” operatörü ile birleştiren ChatActionTypes tipini belirliyoruz.

Şimdi herhangi bir component’ten bu action tiplerini store’a göndermemizi sağlayacak olan fonksiyonları(actions) oluşturmamız gerekiyor. Bu amaçla 3_actions.ts dosyasını oluşturalım:

import {Message} from './1_models';
import {ChatActionTypes, SEND_MESSAGE, DELETE_MESSAGE} from './2_actionTypes';
export function sendMessage(newMessage: Message): ChatActionTypes {
return {
type: SEND_MESSAGE,
payload: newMessage,
};
}
export function deleteMessage(timestamp: number): ChatActionTypes {
return {
type: DELETE_MESSAGE,
meta: {
timestamp,
},
};
}

TypeScript, daha önce iki tipi birleştiğimiz ChatActionTypes’tan ilgili action tipini çıkarabiliyor.

Şimdi bu action’lar store’a geldiğinde ne yapılacağının belirlenmesi için 4_reducers.ts dosyasını oluşturalım.

import {ChatState} from './1_models';
import {ChatActionTypes, SEND_MESSAGE, DELETE_MESSAGE} from './2_actionTypes';
const initialState: ChatState = {
messages: [],
};
export function chatReducer(
state = initialState,
action: ChatActionTypes,
): ChatState {
switch (action.type) {
case SEND_MESSAGE:
return {
messages: [...state.messages, action.payload],
};
case DELETE_MESSAGE:
return {
messages: state.messages.filter(
message => message.timestamp !== action.meta.timestamp,
),
};
default:
return state;
}
}

chatReducer, belirlenen action tipi geldiğinde ona göre messages array’i üzerinde değişiklik yapacaktır.

Şimdi reducer’ları uygulamamıza sunmak için store’umuzu oluşturmamız gerekiyor. Bu aşamada store dizini içerisinde index.ts dosyasını aşağıdaki gibi oluşturabiliriz:

import {combineReducers, createStore, applyMiddleware} from 'redux';
import {chatReducer} from './chat/4_reducers';
import {createLogger} from 'redux-logger';
const rootReducer = combineReducers({
chat: chatReducer,
// OtherReducer
});
export type AppState = ReturnType<typeof rootReducer>;export default function configureStore() {
const middlewares = [createLogger({})];
const middleWareEnhancer = applyMiddleware(...middlewares);
const store = createStore(rootReducer, middleWareEnhancer);
return store;
}

combineReducers fonksiyonu, birden fazla reducer’ı birleştirerek tek bir reducer oluşturmamızı sağlıyor. Daha sonra rootReducer fonksiyonunu bir type olarak kullanmamızı sağlayacak ReturnType tipini kullanıyoruz ve AppState değişkenini üretiyoruz. configureStore() fonksiyonu ile store oluşturarak gerekli middleware’leri ekliyoruz. Şimdi oluşturduğumuz store’u index.tsx içerisinde nasıl kullanacağımıza değinelim:

import React from 'react';
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import configureStore from './src/store';
import {Provider} from 'react-redux';
const store = configureStore();const Root = () => (
<Provider store={store}>
<App />
</Provider>
);
AppRegistry.registerComponent(appName, () => Root);

react-redux’taki Provider component’i ile App’i sarmalayarak, uygulamaya store’u aktarıyoruz. Şimdi App.tsx içerisinde, props’a oluşturduğumuz modelleri ve action’ları map edecek fonksiyonları tanımlayalım:

const mapStateToProps = (state: AppState) => ({
chat: state.chat,
// otherReducer: state.otherReducer,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({sendMessage, deleteMessage}, dispatch);
type AppProps = ReturnType<typeof mapStateToProps> &
ReturnType<typeof mapDispatchToProps>;

mapStateToProps ile props’a oluşturduğumuz model state’lerini verebiliyoruz. mapDispatchToProps ile action’ların store’a dispatch edilmesini (gönderilmesini) sağlayabiliyoruz. Buradaki bindActionCreators fonksiyonu, action’ları dispatch ile bind etmeye yarar. Bu sayede action’lar direkt olarak çağrılabilir. AppProps ile bu iki fonksiyonu birleştirip bir type üretiyoruz.

AppProps’u uygulamada en basit haliyle şu şekilde kullanabiliriz:

const App: React.FC<AppProps> = (props: AppProps) => {useEffect(() => {
props.sendMessage({
user: 'zafer',
timestamp: new Date().getTime(),
text: 'Merhaba',
});
props.sendMessage({
user: 'zafer',
timestamp: new Date().getTime(),
text: 'Naber',
});
}, [ ]);
return (
<SafeAreaView>
<FlatList
data={props.chat.messages}
keyExtractor={item => item.timestamp.toString()}
renderItem={({item}) => (
<Text>{item.text}</Text>
)}
/>
</SafeAreaView>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App);

Burada useEffect fonksiyonu ile store’a mesaj eklenmesi için sendMessage() fonksiyonunu bir kereliğine çağırmış oluyoruz. Şimdilik örnek olması amacıyla useEffect’i ekledim. Daha sonra bunu kaldırarak TextInput ve Button’a atadığımız fonksiyonlar ile yapacağız.

FlatList’in data özelliğine de props’ta bind ettiğimiz chat state’ini vererek mesajların görüntülenmesini sağlıyoruz. Uygulamanın görünümü aşağıdaki gibi olacaktır:

TextInput ve Button ekleyerek uygun şekilde fonksiyonları bağladığımızda App.tsx’in son hali aşağıdaki gibi olacaktır:

import React, {useState} from 'react';
import {
Text,
TextInput,
SafeAreaView,
FlatList,
View,
TouchableOpacity,
KeyboardAvoidingView,
StyleSheet,
} from 'react-native';
import {connect} from 'react-redux';
import {AppState} from './src/store';
import {sendMessage, deleteMessage} from './src/store/chat/3_actions';
import {Message} from './src/store/chat/1_models';
import {bindActionCreators, Dispatch} from 'redux';
const mapStateToProps = (state: AppState) => ({
chat: state.chat,
// otherReducer: state.otherReducer,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({sendMessage, deleteMessage}, dispatch);
type AppProps = ReturnType<typeof mapStateToProps> &
ReturnType<typeof mapDispatchToProps>;
const App: React.FC<AppProps> = (props: AppProps) => {
const initMessage: Message = {
user: 'zafer',
timestamp: new Date().getTime(),
text: '',
};
const [message, setMessage] = useState<Message>(initMessage);
const handleSend = () => {
console.log('Message:' + message.text);
if (message.text === '') {
return;
}
props.sendMessage(message);
setMessage(initMessage);
};
const handleChangeText = (e: string) => {
setMessage({
text: e,
timestamp: new Date().getTime(),
user: 'zafer',
});
};
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
const hours = date.getHours();
const minutes = date.getMinutes();
const hoursText = hours < 10 ? `0${hours}` : hours;
const minutesText = minutes < 10 ? `0${minutes}` : hours;
return `${hoursText}:${minutesText}`;
};
return (
<SafeAreaView style={styles.container}>
<FlatList
data={props.chat.messages}
keyExtractor={item => item.timestamp.toString()}
renderItem={({item}) => (
<View style={styles.messageContainer}>
<Text style={styles.messageText}>{item.text}</Text>
<Text style={styles.messageTime}>{formatTime(item.timestamp)}</Text>
</View>
)}
/>
<KeyboardAvoidingView
enabled={true}
behavior="padding"
style={styles.inputContainer}>
<TextInput
style={styles.textInput}
returnKeyType="send"
onChangeText={handleChangeText}
onSubmitEditing={handleSend}
value={message.text}
/>
<TouchableOpacity style={styles.sendButton} onPress={handleSend}>
<Text style={styles.sendButtonText}>Gönder</Text>
</TouchableOpacity>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App);
const styles = StyleSheet.create({
container: {
margin: 10,
flex: 1,
justifyContent: 'space-between',
},
messageContainer: {
alignSelf: 'flex-end',
alignItems: 'flex-end',
padding: 10,
backgroundColor: '#448aff',
borderRadius: 3,
marginBottom: 5,
flexDirection: 'row',
maxWidth: 300,
},
messageText: {
color: '#fff',
fontSize: 15,
marginEnd: 40,
},
messageTime: {
color: '#fff',
fontSize: 12,
opacity: 0.7,
marginStart: 10,
position: 'absolute',
end: 10,
bottom: 10,
},
inputContainer: {flexDirection: 'row', alignItems: 'center'},
textInput: {
flex: 1,
borderColor: '#448aff',
borderWidth: 1,
padding: 10,
borderRadius: 3,
marginBottom: 20,
},
sendButton: {paddingHorizontal: 10, marginBottom: 20},
sendButtonText: {color: '#448aff'},
});

Sonuç olarak

Uygulamalarda Redux ile TypeScript’in birlikte kullanımı boilerplate code yaratsa da statik tip kontrolü sayesinde uygulamayı daha yayına almadan olası birçok hatanın önüne geçmiş hale geliyoruz. Ben de bu amaçla TypeScript’i projelerinizde kullanmanız için tavsiye ediyorum.

Projenin bitmiş halini react-native-redux-typescript reposunda bulabilirsiniz. Bu yazım hakkında soru ve görüşlerinizi aşağıdaki yorumlar kısmından yazabilirsiniz. Bana destek vermek için alkış simgesine tıklayabilirsiniz. Sonraki yazımda görüşmek üzere…

Kaynaklar:

--

--

No responses yet