によると、
export default function RootLayout(props) {
console.log("props in layout",props)
props.params.newProps = "test"
return (
<div>
{props.children}
</div>
);
}
const Page = (props) => {
console.log("props in page", props.params);
return <></>
}
export default Page
とすれば、
params: { newProps: "test" }
とconsoleがでるはずですが出ませんでした。
そこでObject.assignも試したのですがうまく行きませんでした。もしうまく行った人がいたら教えてください。
対処法
useContextを使用することでLayoutからデータを受け取ることができます。
'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>
)
}
'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に値を受け渡しする方法です。
今回は以下のコードを書いて実行してみました。
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";
を宣言します。
"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";
を宣言します。
"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;
以上のコードを実行して確認してみると、
と、page.tsxで値を受け取ることができました。
では、useStateの値を更新したいと思います。
変更する点は先程コメントアウトしていた以下の文のコメントアウトを外して実行します。
layoutValue.setState("test");
実行結果です。
実際に値は更新されているのですが、以下の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.tsx
でuseLayoutContext()
を呼び出して値を取得してみます。
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;
エラーが発生してしまいました。
どうやら'use client';
下でないと他のファイルからのhooksを呼び出すことはできないようです。
'use client';
をlayout.tsxで宣言して再度実行してみます。
また、
console.log(layoutValue);
も追加しました。('use client';
下なので)
最後にボタンを押したらlayout.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;
実行結果です。
このことからボタンが押されたpage.tsx
内でもう一度読み込まれましたが、layout.tsx
では値が更新されませんでした。
useEffect
でconsole.logを囲ったりしたのですが更新した値を受け取ることができませんでした。
もし他に良い方法があったら教えてください!随時追記していきます。
追記
@honey32 にご指摘いただきました。
layout.tsx
で値を受け取ることができなかったのは単純にLayoutProvider
下でないことが原因でした。
そのため、もしサイドバーの開け締めなどの状態を渡したい場合はlayout.tsx
に直書きするのではなく一度コンポーネントにしてlayout.tsx
のLayoutProvider
の中にサイドバーのコンポーネントを宣言すれば値を使用することができます。
@honey32 さんのコードほとんどそのままですが掲載させていただきます。
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;
'use client';
const SidebarContainer: React.FC = () => {
const layoutValue = useLayoutContext();
// サイドバー等を開閉したりするのに LayoutContext を使える
}
export default SidebarContainer;
これでServer Componentの恩恵を損ねることなくサイドバーの開閉の状態などを受け渡しすることができるようになります。
実行環境
あまり意味ないと思いますが一応載せます。
- node v19.5.0 (npm v9.3.1)
{
"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"
}
}
$ 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