最近気になっているUIライブラリ

最近Chakra-UIにtailswindから乗り換えたんだけど、早速mantineが気になっています。

まぁ、chakra-uiからtailswindに乗り換えた理由はtailswindは結局のところ一からスタイル当てているのと大差がなく、個人的に一番求めていたデザインを統一するのには向いてないなと感じたからです。

Chakra-UIはデザインの統一性を担保したままに、デザインもゴリゴリいじれるという点で気に入ってて、導入してよかったなーと。

ただ、Chakra-UIはステッパーなどのUIがなくてあったら良いのになーと思っていた所、mantineというライブラリに。

まだ、試していないけど、Chakra-UIはなるべく最小のコンポーネント単位で提供してくれているのに対してmantineはもう少しグループ化した単位でコンポーネントを提供してくれてるっぽくて、Chakra-UIの痒いところに手が届かないを解消してくれそう。

まぁ、まだ試せていないので暇ができたら試してみよう。

Atomicデザインで開発を進める上での呟き

みなさん、reactなりvueなりでコンポーネントを作るときにどの様にして開発を進めていますか?

今開発中のアプリは基本的にatomicデザインベースでコンポーネントの管理をしていますが、こんなこと多々ないでしょうか?

「これのコンポーネント作ったっけ?」「既に似たようなコンポーネントあった」「これ使われてるの?」🤔

などなど、独自componentに切り出している場合は、このような疑問が良くあるはずです。

このような現象に陥る原因としては、他にも使えそうという理由で必要以上にコンポーネントを切り出していたり、管理が甘かったりなどなど(ほぼコレ)。

仮に、storybookを使っていても、結局使わないものなどが出てくるので、storybookは解決にはなり得ないかと。

もし、コンポーネント管理をデザイナー主導で行い、コンポーネンの切り出しは要相談の上でドキュメントに残すなどすれば上記のような現象には見舞われないはず。

そこで、最近思っているのがコンポーネントは利用する単位でライブラリに分けてつかえば良いのではということです。

Packageのインストール時に、開発しているアプリに必要なコンポーネントだけを指定してインストールするようにしておけばバンドルサイズも節約できる。 コンポーネントに機能やデザインを追加する場合には、デザイナーと相談の上で行う。 また、無限にpropsが増えていく現象も抑制できることで、一つのコンポーネントに機能てんこ盛りという事態も防げ、テスト自体も機能とデザインでしっかり分離できる。

ただデメリットとしては、ある程度の規模の開発にならないとパッケージの管理コストが掛かってしまうという点で小規模だと正直面倒臭いということですね。

react-queryのinitialDataが便利だなって話

react-queryではuseQueryなどのhooks利用時にinitialDataという初期データを投入することができます。

この初期データというのは、useQueryで取得してくるデータ(厳密にはなんでもいいけど、そんな使い方するならplaceholder使う?)で、すでに何かしらの方法で初期データがあれば、fetchをせずに、その初期データを利用するというもの。

また、queryClientを利用すれば、react-queryで管理しているキャッシュを拾ってくれるので、initialDataと組み合わせればいい感じにパフォーマンスアップを期待できる。

const useHoge = () => {
  ...
  const queryClient = useQueryClient();
  
  const query = useQuery(['hoge'], () => someRequest(), {
    initialData() {
      return queryClient.getQueryData(['hoge'])?.find( data => data.id == someId)
    }
  })

...
}

queryClientのデータを、別ページで取得していたことあるデータから拾ってくるなど、うまいこと組み合わせるといい感じです。

ちなみに、initialDataがundefinedであれば、普通に拾いに行ってくれます。

useQueriesを使ったらsuspenseできなくて困った話

1ページでリクエストを送る数が多く、PromiseでいうところのPromise.allをreact-queryのuseQueriesで利用したらPromiseがthrowされず、fallback画面が表示されなかったという話。

react-queryではデータ取得時のhooksが幾つか提供されいますが、一度に多量のリクエストを捌くものとしてuseQueriesというhooksが提供されています。(useQueryと混同しないように)

で、こちらのhooksですが、useQueryでは提供されているsuspenseモードがブログ投稿時点では対応していない模様です。(githubのissueには上がっていました。

なので、利用する際にはSuspenseで囲んでも動かないので、いい感じに別にPromiseで囲んでSuspenseをuseQueries以外のPromiseに引っ掛かるようにするか(できるか不明だけど多分できそう?)、旧来の方法を使うしかない模様です。

react-query使ってみた

今開発中のアプリはfirebaseを軸に開発しているのだが、パフォーマンスが少々気になっていた。

パフォーマンスと一言で言っても、apiへのリクエスト数とデータが取得されるまでの時間といったデータアクセス部分に関して気になっていたという所。

そういえばキャッシュングしてないなと思っていたところ、少し前に同僚がreact-query良いよって言ってたのを思い出した。

そもそもなぜキャッシュ機構を入れていなかったのかというと、firestoreを使う場合には、デフォルトでキャッシュ機能が付いているからそれで対応すれば良いかーと考えていた。 後にfirestoreは有効期限が設定できないという点が判明して、今すぐ必要なわけじゃないから必要になった時になんとするかーと考えていた。

で、今に至り、結局firestoreを使っている箇所にキャッシュングは必要無くなったのだが、別の箇所で必要になった。

で、早速実装してみたのだが、すごく良い感じだ。

記事執筆時点では、基本的なuseQueryを使った実装を数箇所、既存のコードと入れ替える形でしか使っていないがもっと早く入れておくべきだったなと感じている。

現在利用している形としては、グローバルにrefetchを無くして有効期限だけを設定している。

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnMount: false,
      refetchOnWindowFocus: false,
      suspense: true,
    }
  }
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

上記でwindow focus時とmount時に再度フェッチが走るのを無効にしている。 特にonMount時を無効化しておかないと、ページを遷移して戻ってくるたびに同じデータを取得してしまうので、再フェッチさせる必要のないページでは必須だ。

必要になればその時にrefetchを走らせれば良いだけ。

また、suspenseを有効化しておくことで、react18から使えるSuspenseに対応させることができる。

react-queryの使い方はhooks内もしくはコンポーネント配下で行う。 現プロジェクトはcomponent -> hooks -> repositoriesと構成されているので、基本的にhooks内にカスタムhooksとしてuseQueryを定義する。 ただ、実際のデータアクセス機能に関してはrepositoryに分離しておくことで、仮にreact-queryを使わなくなった場合にもhooksの修正だけで対応できる。ということにした。

const useMyQuery = (userId) => {
  const [user, setUser] = useState()
  const query = useQuery([“myQuery”, userId], () => userRepository.fetchUser(userId),                   {
  onSuccess(result) {
    if(!result.data.user) {
      throw new Error(“what a day!!”)
    }
  }
  })

  useEffect(() => {
    if(!query.data) return;
    setUser(query.data.data)
  }, [query])

  return {query, user}
}


const MyComponent = () => {
  const {user} = useMyQuery()

  return(
    <div>{user.name}</div>
  )
}

const SomeOuter = () => {
  return(
    <Suspense fallback={<div>loading</div>}>
      <MyComponent>
    </Suspense>
  )
}

DRFのシリアライザーでnull値をblankにしたかった

今開発中のアプリでクライアントから受け取ったデータからnullを取り除いて、blankに変更したかった。

流れとしては

  1. クライアントのフォームから値を送信
  2. サーバーで値を受け取る

で、1のクライアントからデータを受け取った際、値に何も入っていないデータはnullで届くようになっているが、serializerのバリデーションで弾かれる。

フォームからの送信時にnullをブランクにしても良かったのだが、とりあえずdjango側でnullをblankにcastすることにした。 値を受け取ってからDRFがvalidationを実行しsaveするまでの大まかな流れは以下である

  1. view
  2. to_internal_value (serializer)
  3. save (serializer)

上記の流れから、nullをblankにキャストする際は1のViewまたは2のserializer内で行うことができる。 値のキャストを担う機能をどこで行うか考えたところ、serializerで行うことにした。

複雑なデータのやりくりであればserializerでやるべきではないのだが、今回のようなシンプルな値の変換はserializer内のto_internal_valueで行うのが最適という結論に至った。 理由としては、2のメソッドはserializerのバリデーション実行前、書き込み時のみに実行されるからだ。 また、serializerの持つfieldを扱うことができるので楽だなと。 ということで、以下のようにコードを実装した。

class User(ModelSerializer):
  class Meta:
    model = User
    fields = "__all__"

  def to_internal_value(self, data):
    cast_blank_fields = ["non_blank_field"]
    fields = self._writable_fields
    for field in fields:
      field_name = field.field_name
      if field_name in cast_blank_fields:
        if not field.get_value(data):
          data[field_name] = ""

    return super().to_internal_value(data)

上記のように、to_internal_valueをオバーライドしつつ、そのベースクラスの同メソッドを呼ぶことで、上書きしたあとにもベースクラスのバリデーションも実行できるという手段である。

DRFのnestedに書き込みができなかった話

プライマリーキーではないが、ユニーク属性のカラムに外部キーを張っていた際に、DRFにネストした状態で一括で保存ができなかった。

viewからSeriializerに渡されるデータは以下を想定している。また、UserSerializerはネスト状態で、子要素の書き込みも同時に行えるようにcreateメソッドをオーバーライドしておいた。

{
    "not_pk": "abc123",
    "name: "tanaka",
    "description": {
        "maximo": "when you are in Rome..."
    }
}
class User(Model):
    not_pk = CharField(null=False, unique=True...

class UserDescription(Model):
    user = ForeignKey(User, related_name="description",...
class UserDescriptionSerializer(ModelSerializer):
    class Meta:
        model = UserDescription
        fields = "__all__"


class UserSerializer(ModelSerializer):
    description = UserDescriptionserializer()

    class Meta:
        model = User
        fields = "__all__"

    def create(self, validated_data):
        user_description_data = validate_data.pop("user_description)
      user = User.objects.create(**validated_data)
      UserDescription.objects.create(user=user, **user_description_data)

      return user

理想でいけばこのままで動くはずだったのだが、postリクエストを投げると以下のようなエラーがでた。

{"description":{"user":["This field is required."]}}

ちなみに、エラーの発生はUserSerialierのcreateより前である。この理由としては、DRFのシリアライザーがデータを受け取ってからcreateメソッドを実行するまでの間に、各フィールに対してのバリデーションをかけるからである。(現場で使えるDjango REST frameworkの教科書 第3章 シリアライザ 参照のこと

そこで、UserSerializerにデシリアライズをするときにuser(必須フィールド)に対してrequired=Falseオプションを与えバリデーションをかけないようにすることで回避した。

class UserDescriptionSerializer(ModelSerializer):
    user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)

    class Meta:
        model = UserDescription
        fields = "__all__"