// src/firebase.tsx
import { getFirestore, collection, doc, setDoc } from 'firebase/firestore';
const db = getFirestore(app);
export const createChannel = async ({
title,
desc,
}: {
title: string;
desc: string;
}) => {
const channelCollection = collection(db, 'channels');
const newChanneRef = doc(channelCollection);
const id = newChanneRef.id;
const newChannel = { id, title, description: desc, createdAt: Date.now() };
await setDoc(newChanneRef, newChannel);
return id;
};
import React, { useState, useRef, useEffect, useContext } from 'react';
import styled from 'styled-components/native';
import { Button, Input, ErrorMessage } from '../components';
import { themeType } from '../theme';
import { StackNavigationProp } from '@react-navigation/stack';
import { MainStackParamList } from '../navigations/Main';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { ProgressContext } from '../contexts';
import { createChannel } from '../firebase';
import { Alert } from 'react-native';
interface ThemeProps {
theme: themeType;
}
const Container = styled.View<ThemeProps>`
flex: 1;
background-color: ${({ theme }) => theme.background};
justify-content: center;
align-items: center;
padding: 0 20px;
`;
const StyledText = styled.Text`
font-size: 30px;
`;
type ChannelCreationScreenNavPropsType = StackNavigationProp<
MainStackParamList,
'ChannelCreation'
>;
type Props = {
navigation: ChannelCreationScreenNavPropsType;
};
const ChannelCreation = ({ navigation }: Props) => {
const [title, setTitle] = useState('');
const [desc, setDesc] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [disabled, setDisabled] = useState(true);
const refDesc = useRef(null);
const { spinner } = useContext(ProgressContext);
useEffect(() => {
setDisabled(!(title && !errorMessage));
}, [title, errorMessage]);
const _handleTitleChange = (title: string) => {
setTitle(title);
setErrorMessage(title.trim() ? '' : 'Please enter the title');
};
const _handleDescChange = (desc: string) => {
setDesc(desc);
setErrorMessage(title.trim() ? '' : 'Please enter the Description');
};
const _handleCreateBtnPress = async () => {
try {
spinner.start();
const id = await createChannel({
title: title.trim(),
desc: desc.trim(),
});
navigation.replace('Channel', { id, title });
} catch (e) {
Alert.alert('Creation Error', e.message);
} finally {
spinner.stop();
}
};
return (
<KeyboardAwareScrollView
contentContainerStyle={{ flex: 1 }}
extraScrollHeight={20}
enableOnAndroid={true}
>
<Container>
<StyledText>Channel Creation</StyledText>
<Input
label="Title"
value={title}
onChangeText={_handleTitleChange}
onSubmitEditing={() => refDesc.current.focus()}
onBlur={() => setTitle(title.trim())}
placeholder="Title"
returnKeyType="next"
maxLength={20}
/>
<Input
ref={refDesc}
label="Description"
value={desc}
onChangeText={_handleDescChange}
onSubmitEditing={_handleCreateBtnPress}
onBlur={() => setDesc(desc.trim())}
placeholder="Description"
returnKeyType="done"
maxLength={40}
/>
<ErrorMessage message={errorMessage} />
<Button
title="Create"
disabled={disabled}
// onPress={() => navigation.replace('Channel')}
onPress={_handleCreateBtnPress}
/>
</Container>
</KeyboardAwareScrollView>
);
};
export default ChannelCreation;
FlatList 를 활용하여 스크롤 되는 화면을 만든다.
ScrollView 컴포넌트와 다른점은 필요한 데이터만 렌더링 하고 나머지는 추후에 렌더링한다.
windowSize : https://reactnative.dev/docs/optimizing-flatlist-configuration
현재화면외에 미리 렌더링할 화면 갯수를 지정
(Ex: 초기화면에 5개가 렌더링 되어 있고 window size가 10개 라면
5(한화면에 렌더링 갯수) * {0(초기화면이라) + 1(현재 화면)+ 10(windowSize)) = 50개 : 초기에 50개가 렌더링 되고 스크롤 내릴수록 렌더링 갯수가 늘어난다.)
변화없는 컴포넌트는 재렌더링을 방지하기 위하여 React.memo()를 이용한다.
// src/screens/ChannelList.tsx
const channels = [];
for (let i = 0; i < 1000; i++) {
channels.push({
id: i,
title: `title: ${i}`,
description: `desc: ${i}`,
createdAt: i,
});
}
const ItemContainer = styled.TouchableOpacity<ThemeProps>`
flex-direction: row;
align-items: center;
border-bottom-width: 1px;
border-color: ${({ theme }) => theme.itemBorder};
padding: 15px 20px;
`;
const ItemTextContainer = styled.View`
flex: 1;
flex-direction: column;
`;
const ItemTitle = styled.Text<ThemeProps>`
font-size: 20px;
font-weight: 600;
color: ${({ theme }) => theme.text};
`;
const ItemDesc = styled.Text<ThemeProps>`
font-size: 16px;
margin-top: 5px;
color: ${({ theme }) => theme.itemDesc};
`;
const ItemTime = styled.Text<ThemeProps>`
font-size: 12px;
color: ${({ theme }) => theme.itemTime};
`;
const ItemIcon = styled(MaterialIcons).attrs(({ theme }) => {
return {
name: 'keyboard-arrow-right',
size: 24,
color: theme.itemIcon,
};
})``;
interface itemProps {
item: {
id: string;
title: string;
description: string;
createdAt: number;
};
onPress: () => void;
}
const Item = React.memo(
({ item: { id, title, description, createdAt }, onPress }: itemProps) => {
console.log(id);
return (
<ItemContainer>
<ItemTextContainer>
<ItemTitle>{title}</ItemTitle>
<ItemDesc>{description}</ItemDesc>
</ItemTextContainer>
<ItemTime>{createdAt}</ItemTime>
<ItemIcon />
</ItemContainer>
);
},
);
const ChannelList = ({ navigation }: Props) => {
return (
<Container>
<StyledText>Channel List</StyledText>
<FlatList
data={channels}
renderItem={({ item }) => <Item item={item} onPress={() => {}} />}
keyExtractor={item => item['id'].toString()}
windowSize={5}
/>
</Container>
);
};
export default ChannelList;