Skip to content

FlatList

Naoto0213 edited this page Jun 27, 2022 · 10 revisions

FlatList

詳細

Object,ArrayなどをList化する
使用ライブラリ: https://reactnative.dev/docs/flatlist

ストーリーブック:

参照コード

FlatList
import React, { useCallback, useMemo } from "react";
import {
  FlatList as OriginalFlatList,
  FlatListProps,
  ListRenderItem,
  StyleSheet,
  View,
} from "react-native";
import { SIDE_PADDING, TOP_PADDING } from "src/components/layouts/ScreenWrapper";
import { Split } from "src/components/layouts/Space";
import NotFound from "src/components/molecules/NotFound";
import { SMALL_PX, X_LARGE_PX } from "src/dict/space";
import { isNotEmpty } from "src/utils";

type BaseFlatListProps<T> = Omit<
  FlatListProps<T>,
  "ListHeaderComponent" | "ListFooterComponent" | "keyExtractor"
>;
export type Props<T> = {
  dividerSize?: number;
  // FIXME: loading の入れ忘れをチェックするために required にしておく
  loading: boolean;
  renderLoading?: ListRenderItem<number>;
  renderLoadingNum?: number;
  ListHeaderComponent?: React.ReactNode;
  ListFooterComponent?: React.ReactNode;
  disableScreenWrapperAdjust?: boolean;
  screenWrapperAdjust?: { side: number; top: number };
  nested?: boolean;
  keyExtractor: FlatListProps<T>["keyExtractor"] | keyof T;
  notFound: React.ReactNode;
} & BaseFlatListProps<T>;

export const useExtendedFlatListProps = <T,>({
  dividerSize = SMALL_PX,
  loading,
  renderLoading: renderLoadingBase,
  renderLoadingNum = 3,
  screenWrapperAdjust = { side: SIDE_PADDING, top: TOP_PADDING },
  disableScreenWrapperAdjust,
  nested,
  keyExtractor: keyExtractorBase = defaultKeyExtractor,
  notFound = "",
  ...baseProps
}: Props<T>): FlatListProps<T> => {
  const screenWrapper = useMemo(
    () => (disableScreenWrapperAdjust || nested ? { side: 0, top: 0 } : screenWrapperAdjust),
    [disableScreenWrapperAdjust, nested],
  );
  const style = useMemo(
    () => [
      styles.flatList,
      { marginHorizontal: -screenWrapper.side, marginTop: -screenWrapper.top },
      baseProps.style,
    ],
    [baseProps.style, screenWrapper],
  );
  const ItemSeparatorComponent = useMemo(
    () =>
      baseProps.ItemSeparatorComponent ??
      (() => <View style={{ width: dividerSize, height: dividerSize }} />),
    [baseProps.ItemSeparatorComponent, dividerSize],
  );
  const loadingData = useMemo(
    () => Array.from({ length: renderLoadingNum }, (v, k) => k as any as T),
    [renderLoadingNum],
  );
  const renderLoading: ListRenderItem<number> = useMemo(
    () => (props) => (renderLoadingBase && renderLoadingBase(props)) ?? null,
    [renderLoadingBase],
  );
  const contentContainerStyle = useMemo(
    () => [
      !nested && { paddingBottom: X_LARGE_PX },
      { marginHorizontal: screenWrapper.side, marginTop: screenWrapper.top },
      baseProps.contentContainerStyle,
    ],
    [baseProps.contentContainerStyle, screenWrapper, nested],
  );
  const keyExtractor: FlatListProps<T>["keyExtractor"] = useCallback(
    (item: T, index: number) =>
      typeof keyExtractorBase === "function"
        ? keyExtractorBase(item, index)
        : `${item[keyExtractorBase as keyof T]}`,
    [keyExtractorBase],
  );
  const ListHeaderComponent = (
    <>
      {baseProps.ListHeaderComponent && (
        <>
          {baseProps.ListHeaderComponent}
          <Split vertical size={dividerSize} />
        </>
      )}
      {!loading &&
        !isNotEmpty(baseProps.data) &&
        (typeof notFound === "string" ? <NotFound message={notFound} /> : notFound)}
    </>
  );
  const ListFooterComponent = (baseProps.ListFooterComponent || null) && (
    <>
      <Split vertical size={dividerSize} />
      {baseProps.ListFooterComponent}
    </>
  );

  const props: FlatListProps<T> = {
    ...baseProps,
    style,
    keyExtractor,
    ItemSeparatorComponent,
    contentContainerStyle,
    ListHeaderComponent,
    ListFooterComponent,
    nestedScrollEnabled: baseProps.nestedScrollEnabled ?? nested,
  };
  if (loading)
    return {
      ...props,
      data: loadingData,
      // @ts-ignore
      renderItem: renderLoading,
      // @ts-ignore
      keyExtractor: numberKeyExtractor,
      getItemLayout: undefined,
      getItem: undefined,
    };
  return props as FlatListProps<T>;
};

const FlatList = React.memo(
  React.forwardRef(<T,>(baseProps: Props<T>, ref: React.Ref<OriginalFlatList>) => {
    const props = useExtendedFlatListProps(baseProps);
    return <OriginalFlatList {...props} ref={ref} />;
  }),
);

const numberKeyExtractor = (i: number) => String(i);
const defaultKeyExtractor: Exclude<FlatListProps<any>["keyExtractor"], undefined> = (v, k) =>
  `${k}`;

// ScreenWrapper の使用下で
// スクロールバーの位置を調整するために, renderItem 側で Margin を再度付与
const styles = StyleSheet.create({
  flatList: {},
});

export default FlatList;
FlatListComponents
const FlatListComponents: React.FC = () => {
  return (
    <FlatList
      data={[0, 1, 2, 3]}
      loading={false}
      keyExtractor={(v) => `${v}`}
      renderItem={({ item }) => (
        <Card>
          <H1>タイトル</H1>
          <P>item: {item}</P>
        </Card>
      )}
    />
  );
};

使い方

基本系

const FlatListComponents: React.FC = () => {
  return (
    <FlatList
      data={[0, 1, 2, 3]}
      loading={false}
      keyExtractor={(v) => `${v}`}
      renderItem={({ item }) => (
        <Card>
          <H1>タイトル</H1>
          <P>item: {item}</P>
        </Card>
      )}
    />
  );
};
スクリーンショット 2022-06-27 15 12 06

手順

  1. dataにList化するObjectを定義
  2. renderItemでReactNodeで出力
  3. notFound時がある場合、notFoundにReactNodeを定義
Property Description
data Object,Array
renderItem dataに定義したObjectをReactNodeで出力
notFound notfound時の表示

Props

DefaultProps:
https://reactnative.dev/docs/flatlist

足りないものはストーリーブックに追加後に追加

Property Description Default types required
data 配列を定義 Object true
renderItem 配列の中身を出力する React.ReactNode true
notFound notfound時の表示 null React.ReactNode true
ListHeaderComponent Listの一番上に表示されるコンポーネント null React.ReactNode false
dividerSize dividerの大きさ 8 number false
loading fetch中はloading画面を表示するか false boolean false
disableScreenWrapperAdjust dividerの大きさ false boolean false
nested fetch中はloading画面を表示するか false boolean false

Clone this wiki locally