LoginSignup
1
0

More than 1 year has passed since last update.

Next.js 13 layout.tsxからpage.tsxにデータを渡す方法

Last updated at Posted at 2023-02-15

によると、

layout.tsx
export default function RootLayout(props) {
  console.log("props in layout",props)
  props.params.newProps = "test"
  return (
        <div>
          {props.children}
        </div>
  );
}
page.tsx
const Page = (props) => {
  console.log("props in page", props.params);
  return <></>
}

export default Page

とすれば、

params: { newProps: "test" }

とconsoleがでるはずですが出ませんでした。
そこでObject.assignも試したのですがうまく行きませんでした。もしうまく行った人がいたら教えてください。

対処法

useContextを使用することでLayoutからデータを受け取ることができます。

layout.tsx
'use client';

impoort { createContext, Dispatch, SetStateAction } from "react"

export const LayoutContext = createContext<{
  setState: Dispatch<SetStateAction<string>>;
  state: string
}>();

export const RootLayout = (children) => {

  const [setState, state] = useState<string>("")
  const contextValue = {
    setState: setState,
    state: state,
  };

  return (
    <LayoutContext.Provider value={contextValue}>
      {children}
    </LayoutContext.Provider>
  )
}
page.tsx
'use client';

import { LayoutContext } from '@/app/layout';
import { useContext } from "react";


const Page = () => {
  const layoutContext = useContext(LayoutContext);
  layoutContext.setState("テスト")

  console.log(layoutContext.state);

  return <></>;
}

ただしSSRの恩恵を受けることができなくなるのでNext.js 13の意味がなくなってしまう可能性があります。

appディレクトリを使用しないほうが良いかと思います。

補足

@honey32 さんからアドバイスいただきました。
RootLayoutでcontextを宣言せず、ほかの'use client';下で宣言したcontextを使用することでServer Componentの恩恵を損ねることなくlayout.tsxからpage.tsxに値を受け渡しする方法です。
今回は以下のコードを書いて実行してみました。

layout.tsx
import { ReactNode } from "react";
import LayoutProvider from "./SomethingProvider";

const RootLayout = ({ children }: { children: ReactNode }) => {
  return (
    <html>
      <body>
        <LayoutProvider>{children}</LayoutProvider>
      </body>
    </html>
  );
};

export default RootLayout;

SomethingProvider.tsxを作成し、そこにフックを作成します。
ここではuse~~~を使用しますのでこのファイルは"use client";を宣言します。

SomethingProvider.tsx
"use client";

import {
  createContext,
  Dispatch,
  ReactNode,
  SetStateAction,
  useContext,
  useState,
} from "react";

export const LayoutContext = createContext<{
  setState: Dispatch<SetStateAction<string>>;
  state: string;
}>({
  setState: function () {} as Dispatch<SetStateAction<string>>, // 冗長で申し訳ございません。
  state: "",
});

export const useLayoutContext = () => {
  return useContext(LayoutContext);
};

const LayoutProvider = ({ children }: { children: ReactNode }) => {
  const [state, setState] = useState<string>("");
  const contextValue = {
    setState: setState,
    state: state,
  };

  return (
    <LayoutContext.Provider value={contextValue}>
      {children}
    </LayoutContext.Provider>
  );
};

export default LayoutProvider;

最後にpage.tsxです。ここではSomethingProvider.tsxからuseLayoutContextをimportして使用するため、"use client";を宣言します。

page.tsx
"use client";

import { useLayoutContext } from "./SomethingProvider";

const Page = () => {
  const layoutValue = useLayoutContext();

  // layoutValue.setState("test");
  console.log(layoutValue);

  return <>This page is Page component.</>;
};

export default Page;

以上のコードを実行して確認してみると、
Screenshot from 2023-02-19 11-50-13.png
と、page.tsxで値を受け取ることができました。
では、useStateの値を更新したいと思います。
変更する点は先程コメントアウトしていた以下の文のコメントアウトを外して実行します。

page.tsx
layoutValue.setState("test");

実行結果です。
Screenshot from 2023-02-19 11-53-31.png
実際に値は更新されているのですが、以下のWarningが出てしまいました。
Warning: Cannot update a component (LayoutProvider) while rendering a different component (Page). To locate the bad setState() call inside Page, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
翻訳すると

警告 異なるコンポーネント(ページ)をレンダリングしている間は、コンポーネント(LayoutProvider)を更新することができません。ページ内の悪い setState() 呼び出しを見つけるには、 https://reactjs.org/link/setstate-in-render で説明されているように、スタックトレースをたどります。

とあり、https://reactjs.org/link/setstate-in-render にアクセスするとgithubのissueに飛びました。
あまり深く拝見していないのですが、childrenからlayoutの値を更新することが良くなかったのかもしれません。
ですが、一応値は更新されているのでワーニングに目を瞑るとします。
layout.tsx"use client";なしに値を受け取り、サイドバーの開け締めなどに使うことができればOKです。

補足の補足
@honey32 さんにまた教えていただきました。
ここでの問題はレンダリングの途中(コンポーネントの関数そのものの実行中) で直に更新しているのが問題とのことでした。
解決方法は

  • useEffect内で更新する
  • onClickなどのイベントハンドラ内で更新する

です。ここではuseEffectは紹介しませんが、下でonClickイベントで更新しています。
一回目の補足の投稿時には気づきませんでしたが、たしかにワーニングは消えていました。

layout.tsxuseLayoutContext()を呼び出して値を取得してみます。

layout.tsx
import { ReactNode } from "react";
import LayoutProvider, { useLayoutContext } from "./SomethingProvider";

const RootLayout = ({ children }: { children: ReactNode }) => {
  const layoutValue = useLayoutContext();
  return (
    <html>
      <body>
        <LayoutProvider>{children}</LayoutProvider>
      </body>
    </html>
  );
};

export default RootLayout;

実行してみます。
Screenshot from 2023-02-19 12-03-28.png

エラーが発生してしまいました。
どうやら'use client';下でないと他のファイルからのhooksを呼び出すことはできないようです。

'use client';をlayout.tsxで宣言して再度実行してみます。
また、

layout,tsx
console.log(layoutValue);

も追加しました。('use client';下なので)

Screenshot from 2023-02-19 12-06-51.png
エラーは消えて受け取ることができました。

最後にボタンを押したらlayout.tsxでも値が更新されるか確認してみます。
変更したコードは以下の通りです。

page.tsx
"use client";

import { useLayoutContext } from "./SomethingProvider";

const Page = () => {
  const layoutValue = useLayoutContext();
  console.log(layoutValue);

  return (
    <button type="button" onClick={() => layoutValue.setState("ボタンがクリックされました。")}>ボタン</button>
  );
};

export default Page;

実行結果です。
Screenshot from 2023-02-19 12-13-07.png
このことからボタンが押されたpage.tsx内でもう一度読み込まれましたが、layout.tsxでは値が更新されませんでした。
useEffectでconsole.logを囲ったりしたのですが更新した値を受け取ることができませんでした。

もし他に良い方法があったら教えてください!随時追記していきます。

追記

@honey32 にご指摘いただきました。
layout.tsxで値を受け取ることができなかったのは単純にLayoutProvider下でないことが原因でした。
そのため、もしサイドバーの開け締めなどの状態を渡したい場合はlayout.tsxに直書きするのではなく一度コンポーネントにしてlayout.tsxLayoutProviderの中にサイドバーのコンポーネントを宣言すれば値を使用することができます。
@honey32 さんのコードほとんどそのままですが掲載させていただきます。

layout.tsx
import { ReactNode } from "react";
import LayoutProvider from "./SomethingProvider";
import SidebarContainer from "./SidebarContainer";

const RootLayout = ({ children }: { children: ReactNode }) => {
  return (
    <html>
      <body>
        <LayoutProvider>
          {children}
         <SidebarContainer /> {/* 追加 */}
        </LayoutProvider>
      </body>
    </html>
  );
};

export default RootLayout;
src/app/SidebarContainer.tsx
'use client';

const SidebarContainer: React.FC = () => {
  const layoutValue = useLayoutContext();

  // サイドバー等を開閉したりするのに LayoutContext を使える
}

export default SidebarContainer;

これでServer Componentの恩恵を損ねることなくサイドバーの開閉の状態などを受け渡しすることができるようになります。

実行環境

あまり意味ないと思いますが一応載せます。

  • node v19.5.0 (npm v9.3.1)
package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@next/font": "13.1.6",
    "@types/node": "18.13.0",
    "@types/react": "18.0.28",
    "@types/react-dom": "18.0.11",
    "eslint": "8.34.0",
    "eslint-config-next": "13.1.6",
    "next": "13.1.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "4.9.5"
  }
}
shell
$ uname --all
Linux haruki 6.1.10-hardened1-1-hardened #1 SMP PREEMPT_DYNAMIC Tue, 07 Feb 2023 19:30:39 +0000 x86_64 GNU/Linux

$ cat /etc/os-release
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
LOGO=archlinux-logo

$ google-chrome-stable --version
Google Chrome 110.0.5481.77
1
0
6

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
0