Instagram Clone : React Native - part 3 [ FEED]


Tab Navigator


Create Tabs


複数のラベルを作成します.スタック同様、Tabs.Navigator内に必要なTabs.Screenを入れる.Screen全体適用オプションのためにNavigatorscreenOptionsそれぞれoptionsScreenの道具に入れる.複数のオプションは、次の正式なドキュメントで参照できます.
TabIcon タブにアイコンを挿入するには、tabBarIconというオプションを使用します。戻り要素の関数を受け入れます。関数のパラメータとして、次の値が得られます。 フォーカスフォーカス:タブを選択するとtrueに戻ります。 color:アイコンの色。tabBarActiveTintColorと同じです。 size:size(number受信) 万博ベクトル-iconリスト @expo/vector-icons directory
関数は繰り返し使用されるので、繰り返し使用可能な素子にすることが望ましい.
// TabIcon.tsx
...
export default function TabIcon({ focused, iconName, color }: TabIconProps) {
  return (
    <Ionicons
      name={focused ? iconName : `${iconName}-outline`}
      color={color}
      size={focused ? 24 : 20}
    />
  );
}

// LoggedInNav.tsx
...
function LoggedInNav() {
  return (
    <Tabs.Navigator ... />
      <Tabs.Screen
	...
        options={{
          tabBarIcon: ({ focused, color, size }) => (
	    // 컴포넌트화 전
            <Ionicons name={focused ? "home" : "home-outline"} 
	     color={color} size={focused ? 24 : 20} />
          ),
        }}
      />
      <Tabs.Screen
	...
        options={{
          tabBarIcon: ({ focused, color, size }) => (
	  // 컴포넌트화 후
            <TabIcon focused={focused} color={color} iconName="search" />
          ),
        }}
      />
      ...

Stack & Tabs


Stacks for each Tab


INSTAGRAMアプリケーションの構成を考慮すると,各タグ内に複数のスタックがある.各ラベルをクリックすると、最初に表示されるスタックがそのラベルのページに移動し、ページ内で写真またはプロファイルをクリックすると別のスタックに移動します.

この構成をコードにするには、タブ(Tabs.Screen)内でコールバックスタック素子(SharedStackNav)の関数をchildrenに送信する必要がある.
// LoggedInNav.tsx
...
function LoggedInNav() {
  return (
    <Tabs.Navigator ...>
      <Tabs.Screen
        name="RootFeed"
	...
      >
        {() => <StackNavFactory screenName="Feed" />}
      </Tabs.Screen>
	...
SharedStackNavエレメント内で支柱を介して伝達されるscreenNameに従って、対応するスタック(タブの最初の画面)を返します.ProfilePhotoすべてのタブに共通のスタック.
// SharedStackNav.tsx
...
const getFirstScreen = (screenName: string) => {
  if (screenName === "Feed") {
    return <Stack.Screen name="Feed" component={Feed} />;
  } else if (screenName === "Search") {
    return <Stack.Screen name="Search" component={Search} />;
  } else if (screenName === "Notifications") {
    return <Stack.Screen name="Notifications" component={Notifications} />;
  } else if (screenName === "Me") {
    return <Stack.Screen name="Me" component={Me} />;
  } else {
    return null;
  }
};

function SharedStackNav({ screenName }: SharedStackNavProps) {
  return (
    <Stack.Navigator ...>
      {getFirstScreen(screenName)} // ** 탭의 첫 번째 화면   
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Photo" component={Photo} />
    </Stack.Navigator>
  );
} 
Error : Found screens with the same name nested inside one another. Check:
親と子の要素の名前が同じ場合に発生するエラーは、異なるエラーに変わります.

Screen Title to Image


Feed screenのタイトルをInstagramのロゴに変更選択可能headerTitleStringまたはReact.NodeImageとともにサイズを頭部に調整します.
// SharedStackNav.tsx
...
const getFirstScreen = (screenName: string) => {
  if (screenName === "Feed") {
    return (
      <Stack.Screen
        ...
        options={{
          headerMode: "screen",
          headerTitle: () => (
            <Image
              style={{ maxHeight: 40, maxWidth: 120 }}
              resizeMode="contain"
              source={require("../assets/logo.png")}
            />
          ),
        }}
      />
    );
  ...
};
...

Apollo Auth


setContext in Apollo Client


Webに示すように、すべてのリクエストヘッダにタグを含めることができます.
FEED(ApolloクライアントのHeader-setContextを参照)
ただし、違いがある場合は、Webはタグをローカルストレージから抽出してヘッダーに配置し、この機会はタグをReactive Variables変数に配置します.
// Apollo.ts
...
const AuthLink = setContext((_, { headers }) => {
  return {
    headers: {
      ...headers,
      token: tokenVar(), // 현재 tokenVar에 저장된 토큰을 불러옴
    },
  };
});

FlatList


ScrollView vs FlatList


Scroll View


Nativeでは、自分だけにスクロールすることはできませんViewこの素子内に、スクリーンより大きいものがあればScrollViewスクロール可能です.
// 상하 스크롤
...
return (
  <View ...>
    <ScrollView>
      <View style={{ height: 20000, flex: 1, backgroundColor: "blue" }}>
        <Text>I'm super big</Text>
      </View>
    </ScrollView>
  </View>
);

// 좌우 스크롤
...
return (
  <View ...>
    <ScrollView horizontal>
      <View style={{ width: 20000, flex: 1, backgroundColor: "blue" }}>
        <Text>I'm super big</Text>
      </View>
    </ScrollView>
  </View>
 );
}

FlatList


ただし、アプリケーションの性能を考慮すると、大量のデータを同時にレンダリングすることはできないViewこの場合、不活性なロードが必要です(コンポーネントがスクリーンにある場合はレンダリング)、ない場合はレンダリングされません.ScrollViewコンポーネントが簡単に実現できる.FlatList3種類の支柱を要求する.
  • data:レンダリングするデータアレイ
  • keyExtracter:各要素を区別するための一意の識別子(=key,stringタイプ)
  • renderItem:レンダリング要素の関数
  • function Feed({ navigation }: FeedProps) {
      const { data, loading } = useQuery(FEED_QUERY);
      const renderPhoto: ListRenderItem<seeFeed_seeFeed || null> = 
    	({ item: photo }) => (
        <View style={{ flex: 1 }}>
          <Text style={{ color: "white" }}>{photo.caption}</Text>
        </View>
      );
      return (
        <ScreenLayout loading={loading}>
          <FlatList
            data={data?.seeFeed}
            // string
            keyExtractor={(photo) => "" + photo.id}
            renderItem={renderPhoto}
          />
        </ScreenLayout>
      );
    }

    Photo


    Rendering Photos


    UseWindowDimensions


    ネイティブで画像をレンダリングするにはwidthとheightの値を指定する必要があります.幅と高さを任意に指定すると、画像のスケールが異常になる可能性があります.Instagramでは画面の幅と同じ幅にします.FlatList方法は画面の幅と高さを求めることができる.

    Image.getSize


    画像ファイルの実際のサイズを取得できます.3つの因子を受ける.
  • 画像ファイル
  • 画像サイズの取得に成功した場合に実行する関数(幅、高さ)
  • 失敗時に実行する関数
  • Render


    上記の2つの方法で、写真をより大きなサイズにレンダリングします.幅はスクリーンの幅に等しく、高さは実際の画像の高さに等しく、大きすぎると3に分けられます.
    // Photo.tsx
    ...
    function Photo(...) {
      const { width, height } = useWindowDimensions();
      const [imageHeight, setImageHeight] = useState(height - 450);
    
      useEffect(() => {
        Image.getSize(file, (width, height) => {
          setImageHeight(height / 3);
        });
      }, [file]);
    
      return (
        <Container>
          ...
          <File
            resizeMode="cover"
            style={{ width, height: imageHeight }}
            source={{ uri: file }}
          />
          ...
        </Container>
      );
    }

    Navigation


    Photoコンポーネントでは、プロファイル、コメント、賛などをクリックして該当する画面に移動できます.しかし、Photoは画面上のコンポーネントなのでnavigation propはありません.2つの方法があります.
  • navigation prop FeedからPhotoへ降格
  • UseWindowDimensions使用
  • 
    const navigation = useNavigation<NavigationProp<RootStackParamList>>();

    Pull to Refresh


    Pull to Refresh


    使用useNavigation素子のFlatListrefreshing道具とonRefreshuseQuery
  • リフレッシュ:リフレッシュ可能(ブール型)
  • onRefresh:リフレッシュ時に実行する関数(関数タイプ)
  • refetchqueryを再呼び出しする関数です.refetchを状態にするrefreshing後にリフレッシュを終了する関数を作成し、それを伸ばしてリフレッシュを実現します.
    // Feed.tsx
    ...
    function Feed(...) {
      const { ..., refetch } = useQuery(FEED_QUERY);
    
      const refresh = async () => {
        setRefreshing(true);
        await refetch();
        setRefreshing(false);
      };
    
      const [refreshing, setRefreshing] = useState(false);
      return (
        <ScreenLayout loading={loading}>
          <FlatList
            refreshing={refreshing} 
            onRefresh={refresh} 
            showsVerticalScrollIndicator={false} // 스크롤바 숨기기
          />
          ...
        </ScreenLayout>
      );
    }

    Infinite Scrolling


    Infinite Scrolling


    クライアントが最後までスクロールすると、seeFeedリクエストがサーバに送信され、データの受信が続行されます.表示されたデータのインデックスはrefetchであり、2つのインデックスを受信したデータのみを格納する.
    では、2枚の写真のうち最後の写真の末尾に到達したときに、offsetをサーバに転送し、次の2枚の写真を再び受け取る.無限反復の機能は,この過程を示す写真がなくなるまで無限スクロールである.

    seeFeed queryの変更

    offsetの価格で、2枚の写真を作成する順番でコードを送信します.
    // SeeFeed.resolvers.ts
    ...
    const resolvers: Resolvers = {
      Query: {
        seeFeed: protectedResolver((_, { offset }, { loggedInUser }) =>
          // photo를 찾을 때, 팔로워 목록에 내 이름이 있는 유저들의 photo를 찾음
          client.photo.findMany({
            take: 2, // 찾을 photo의 개수
            skip: offset, // 나머지 하나는 offset으로 표시 안함
    	...
    
    // SeeFeed.typeDefs.ts
    ...
      type Query {
        seeFeed(offset: Int!): [Photo] // offset variables 추가
      }
    ...

    fetchMore


    クライアントは、特定のスクロール位置でseeFeedリクエストを送信し続けます.offsetFlatListonEndReachedonEndReacedThresholduseQueryを用いて体現する.次の機能があります.
  • onEndReached:ある位置までスクロールして実行する関数
  • onEndReacedThreshold:onEndReached運転の位置(0=スクロールの終了、0より大きい)
  • fetchMore:既存のfetchを保持しながらより多くのデータを取得(ページング実装用)
  • // Feed.tsx
    ...
    export const FEED_QUERY = gql`
      query seeFeed($offset: Int!) {
        seeFeed(offset: $offset) {
        ...
      }
        ...
    `;
    
    function Feed(...) {
      const { ..., fetchMore } = useQuery<
        seeFeed,
        seeFeedVariables
      >(FEED_QUERY, {
        variables: { offset : 0 }, // 초기 값 : 0번째 사진부터
      }); 
      ...
      return (
        <ScreenLayout loading={loading}>
          <FlatList
            onEndReached={() =>
              fetchMore({
                variables: {
    	      // 지금까지 받아온 피드의 수 만큼 스킵
                  offset: data?.seeFeed?.length, 
                },
              })
            } 
            onEndReachedThreshold={0.02}
            ...
          />
        </ScreenLayout>
      );
    }
    これで、サーバは新しいフィードバックを受信し続けますが、画面には何の変化もありません.コンポーネントやステータスの変化がないため、何もレンダリングされません.

    タイプポリシーの設定


    Apollo cacheのタイプを設定します.Apolloは転送パラメータ(offset)に従ってクエリーを独立した空間に格納するため、リストはレンダリングされません.
    FlatList : [seeFeed.offset : 0] // 실제 렌더링 되는 리스트 
    [seeFeed.offset : 2] // 계속해서 피드를 새로 받고있지만 업데이트 되지 않음
    [seeFeed.offset : 4]
    [seeFeed.offset : 6]
    伝達パラメータに基づいてqueryが区別されないようにseeFeedに限定してtypePollicesを設定します.新しい種が加われば既存種と結合して形成できるfetchMore.
    // apollo.ts
    ...
    const client = new ApolloClient({
     ...
      cache: new InMemoryCache({
        typePolicies: {
          Query: {
            fields: {
    	   // seeFeed에 한하여,
              seeFeed: {
    	    // 전달인자에 따라 cache를 구별하지 않는다.
                keyArgs: false, 
    	    // 새로운 데이터를 어떻게 처리해야하는지 알려줌 
    	    // 기존 데이터나 유입 데이터 둘 중 하나만 존재할 수 있으니 빈 배열이 기본 값
                merge: (existing = [], incoming = []) => [...existing, ...incoming],
              },
    	  ...

    offsetLimitPagination


    上記手順は、自機内蔵関数fetchMoreにも簡単に設定できます.
    // apollo.ts
    ...
    typePolicies: {
      Query: {
        fields: {
          seeFeed: offsetLimitPagination(),
          ...

    Cache Persist


    Cache Persist


    サーバとの接続が切断されていても、キャッシュに格納されているデータの限られた領域でアプリケーションを使用できます.Cache使用offsetLimitPagination同期メモリに保存し、AppLoadingにロードすればよい.

    Export Cache


    キャッシュをグローバルにエクスポートして、プリロード時にロードします.
    // apollo.ts
    export const cache = new InMemoryCache({ ... });
    
    const client = new ApolloClient({
      link: AuthLink.concat(httpLink),
      cache,
    });

    Persist Cache


    プリロードすると、保存されたキャッシュがロードされます.また、新しいcacheはpersistCacheに格納するように設定することができる.AsyncStorage初期化する前に、cacheを読み込まなければならない.
    // App.tsx
    
    const preload = async () => {
    	...
        await persistCache({
          cache,
          storage: new AsyncStorageWrapper(AsyncStorage),
        });
        return ...
      };
    
      if (loading) {
        return (
          <AppLoading
            startAsync={preload}
    	...
          />
        );
      }
    
      return (
        <ApolloProvider client={client}>
    	...
        </ApolloProvider>
      );
    }