1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クライアントの子コンポーネントから サーバーの親コンポーネントに 値を渡したいときはどうするか

Posted at

1. はじめに

親から子へはpropsを通じて比較的簡単に値を渡すことができると思います。
親子がクライアントコンポーネントの場合における、子から親への値の受け渡し(逆流)は、二節で紹介するように幾つか方法があります。しかし、親がサーバーコンポーネントの場合における逆流の方法は、あまり紹介されていないように思います。
私もこれには少し頭を悩ませていたのですが、直近のプロダクト制作のなかで解決策を見つけましたので、忘備録も兼ねて共有したいと思います。

目次

2. クライアントの親子の場合

まず、クライアントコンポーネント同士での値の受け渡しを考えます。
子コンポーネントで取得した値を、親コンポーネントに渡す方法は四つほど考えられます。

一つ目は、親コンポーネントで定義したsetState(dipatch関数)を子コンポーネントに渡して、子コンポーネントのフォームで入力した値をそのままsetStateで受け取り、親コンポーネントのstateに反映させる方法です。割とオーソドックスで、色々なところでまずこれが紹介されていると思います。

二つ目は、逆に子コンポーネントで定義したstateをJSX.Elementと一緒に親コンポーネントへ渡してそこで利用する方法です。オブジェクトないし配列としてJSX.Elementと一緒にstateを返します。

三つめはグローバルステートを定義する方法。子から親への一階層分の受け渡しに際して、グローバルステートをわざわざ定義することは少ないと思いますが、孫や曾孫から渡したいときなど階層を重ねての受け渡しに際しては有力な選択肢になってくると思います。

そして最後に、sessionStorageを利用する方法。子コンポーネントで受け取った値をブラウザのストレージに保管し、親コンポーネントが関数の実行時など必要に応じてブラウザストレージから取得する方法です。
sessionStorageを使う方法は、親コンポーネントで何かしらの関数を実行するだけならともかく、親コンポーネントで動的にstorageを取得することは難しいかもしれません。
私はsetIntervalで何秒かおきにストレージを走査することで、親コンポーネントが子コンポーネントの値を動的に取得しているように見せかけるくらいしか思いつきません。かなり苦しい方法ですし、オブジェクトは取り扱いが難しくなるので、あまりお勧めはできないと思います。

3. 親がサーバーコンポーネントの場合

さて、上に四つあげた手段のうち、何が使えるでしょうか。
まず、サーバーコンポーネントなわけですからReact Hooksもウェブストレージも使えません。そのため、親コンポーネントではuseStateもグローバルステートの利用もできないわけですね。
では、子コンポーネントからサーバー親コンポーネントへstateを渡してみるのはどうでしょうか。
例えば、以下の通りです。

Child.tsx
"use client";
import {useState} from "react";

export default function Child():[state,()=>JSX.Element]{
    const [state,setState] = useState();
    const element = () => (
        <div>
            <input onChange={(e) => setState(e)} />
        </div>
    );
    return [state,element];
}
Parent.tsx
import Child from "@/components";

export async function Parent(){
    const threads = await getThreads();
    const [state,element] = Child();
    threads.title.filter(title => title.match(state))
    return(
        <>
            {element()}
            {threads.map((thread) => (
                <h1>{}</h1>
                <p>{}</p> 
            ))}
        </>
    )
}

なんだか行けそうですね。

しかし、このようなエラーが出ました。
Error: Attempted to call the default export of C:Files\Child.tsx from the server but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.

はい。
コンパイラーも、このようなエラーは想定しているのか、エラー文がえらい丁寧ですね。
エラー文を見てみればわかる通り、このエラーはクライアントコンポーネントからサーバーコンポーネントに関数を渡すことはできないよと言っています。
ふむ。
JSX.Elementがアロー関数の返り値にしたのが悪かったのでしょうか?
では、Childを以下のように書き換えてみましょう。

Child.tsx
"use client";
import {useState} from "react";

- export default function Child():[state,()=>JSX.Element]{
+ export default function Child():[state,JSX.Element]{
    const [state,setState] = useState();

-   const element = () => (
+   const element = (
        <div>
            <input onChange={(e) => setState(e)} />
        </div>
    );
    return [state,element];
}

しかし、このような場合でも同じエラーが発生してしまいます。
やはりクライアントの関数コンポーネントは、JSX.Elementの渡し方に関係なく、サーバーコンポーネントのなかで呼び出すことはできないようです。
実際、Parentコンポーネントに"use client"宣言をつけると、上記のコードは無事に動きました。

4. サーバーコンポーネントはサーバーコンポーネントのままで

ここからが本題です。
もし、"use client"宣言をしたスクリプトで関数コンポーネントをつくるのであれば、別に悩むことはないはずです。
二節で紹介したように、従来通りクライアントコンポーネントの親子間でのデータのやり取りをすればいいわけです。しかし、それでは近年実装されたサーバーコンポーネントの利点を生かすことができません。
サーバー側でデータをフェッチして、ページロードの前にデータを取得しておいたり、センシティブなデータをコンポーネントの中で扱ったりすることができなくなりします。これはちょっと不便です。

そこでクライアント子コンポーネントからサーバー親コンポーネントへのデータの受け渡しの手法として考えられるのが、クエリーパラメータの設定です。

Next.jsで、クエリーパラメータを設定する。

例えば、ボタンを押した時に、データの表示をかえるようにしたいとするとき、以下のようにすることができます。

Child.tsx
"use client";
import { usePathname, useSearchParams,router} from "next/navigation";

export default function ChangeThreadDisplay() {
  const searchParams = useSearchParams();
  const params = new URLSearchParams(searchParams);
  const pathname = usePathname();
  const router = useRouter();

  const display = params.get("display") || "0";

  function handleDisplay(display: string) {
    params.set("display", display);
    router.replace(`${pathname}?${params.toString()}`);
  }

  return (
    <div>
      <button 
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" 
          onClick={() => handleDisplay("0")}
        >
        My thread
      </button>
      <button 
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" 
          onClick={() => handleDisplay("1")}
          >
        My Question
      </button>
      <button 
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" 
          onClick={() => handleDisplay("2")}
          >
        My comment
      </button>
    </div>
  );
}

new URLSearchParamsとは、JavaScriptで使えるURL apiの一つです。文字列情報をクエリー文字列が現れるのと同じ順番で[key,value]の配列にしてくれる優れものです。元はURL文字列であるためtoString()で文字列に変換することで、usePathnameで取得した現在のページのパスの後にそのまま付け加えることができます。

Parent.tsx
export default async function MyPage({
  searchParams,
}: {
  searchParams?: {
    display?: string;
  };
}) {
  const display = Number(searchParams?.display) || 0;
  const id = (await User()).id;
  const myThreads = (await getThreads()).filter(thread => thread.user.id === id);
  const myQuestions = (await getQuestions()).filter(questions => thread.user.id === id);
  const myComments = (await getComments()).filter(comment => thread.user.id === id);
  return(
      <div>
          {display === 0 ? myThreads.map(thread => (
              <p>thread.content</p>
            )) : display === 1 ? myQuesitons.map(question => (
              <p>question.content</p>
            )) : myComments.map(comment => (
              <p>comment.content</P>
            ))}
      </div>
      );
}

サーバーコンポーネントでは、クエリーパラメータを取得する機能がデフォルトで備わっています。
propsの形で受け取ることによって、無事クエリーパラメータ(ユーザーが選択した表示の変更)を親のサーバーコンポーネントに渡すことができました。

サーバーコンポーネントは、React Server Component Payloadと言って、サーバーコンポーネントのレンダリング結果をBlob形式にしてクライアントに送られるのだそうです。そして、クライアントのJavaScriptにてクライアントコンポーネントとハイドレーションし、ページが完全な状態で表示されることになります。

Next.jsがApp Routerに変更になってあまり気にしてきませんでしたが、Pages Routerの場合でいうSSG、ISR、SSRの挙動はfetch処理や、このサーバーコンポーネントの設定で行うことができます。

デフォルトでは、Server ComponentはStatic Renderingであり、ビルド時の情報を単に表示するだけです(SSG)。しかし、Dynamic Renderingにすることによって、動的に情報を更新し、サーバーがユーザーのアクセスに応えてくれるようになります(SSR)。
Dynamic Renderingにかわるのは、fetchの時に定めるキャッシュ設定や、searchParamsなどのDynamic Functionを実装した時に、自動的に動態化されます。

つまり、普通は静態的であるはずのParent サーバーコンポーネントは、searchParamsを用いていたために動的な状態へと変更され、Child クライアントコンポーネントで変更されたクエリーの変化に対して即座に反応することができたのです。
これがなければ、sessionStorageの問題にあったように、動的な更新ができなくて詰むところだったというわけです。

5. おわりに

クライアントコンポーネントにおける親子間のデータの逆流については記事が多くありましたが、サーバーコンポーネントの親に対するデータの受け渡しについてはあまり記事がなかった気がするので、この記事が役に立てれば幸いです。

なお、言うまでもないことだとは思いますが、クライアントコンポーネントで表示される他のコンポーネントはすべてクライアントコンポーネントになるため、
即ち、コンポーネントツリー上で、クライアントコンポーネントの下に位置するものはすべてクライアントコンポーネントになるため、
親がクライアントコンポーネントで子がサーバーコンポーネントの場合は親子共にクライアントコンポーネントの場合となんら変わりません。
そのため、頭を悩ませるとすれば親がサーバーコンポーネントの場合だと考えられるわけです。

6. 参考

Next.jsドキュメント様様です。
Server and Client Components
Server Components
Adding Search and Pagination

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?