Skip to content

Commit 13d679f

Browse files
committed
react-hooksのlintエラーを半分修正
1 parent dce00d6 commit 13d679f

File tree

7 files changed

+75
-90
lines changed

7 files changed

+75
-90
lines changed

app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
3+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
44
import { ChatForm } from "./chatForm";
55
import { StyledMarkdown } from "@/markdown/markdown";
66
import { useSidebarMdContext } from "@/sidebar";
@@ -32,11 +32,41 @@ export function PageContent(props: PageContentProps) {
3232
const { setSidebarMdContent } = useSidebarMdContext();
3333
const { splitMdContent, pageEntry, path, chatHistories } = props;
3434

35-
const initDynamicMdContent = useCallback(() => {
35+
const [sectionInView, setSectionInView] = useState<boolean[]>([]);
36+
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
37+
useEffect(() => {
38+
const handleScroll = () => {
39+
setSectionInView((sectionInView) => {
40+
sectionInView = sectionInView.slice(); // Reactの変更検知のために新しい配列を作成
41+
for (
42+
let i = 0;
43+
i < sectionRefs.current.length || i < sectionInView.length;
44+
i++
45+
) {
46+
if (sectionRefs.current.at(i)) {
47+
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
48+
sectionInView[i] =
49+
rect.top < window.innerHeight * 0.9 &&
50+
rect.bottom >= window.innerHeight * 0.1;
51+
} else {
52+
sectionInView[i] = false;
53+
}
54+
}
55+
return sectionInView;
56+
});
57+
};
58+
window.addEventListener("scroll", handleScroll);
59+
handleScroll();
60+
return () => {
61+
window.removeEventListener("scroll", handleScroll);
62+
};
63+
}, []);
64+
65+
const dynamicMdContent = useMemo(() => {
3666
const newContent: DynamicMarkdownSection[] = splitMdContent.map(
37-
(section) => ({
67+
(section, i) => ({
3868
...section,
39-
inView: false,
69+
inView: sectionInView[i],
4070
replacedContent: section.rawContent,
4171
replacedRange: [],
4272
})
@@ -94,54 +124,13 @@ export function PageContent(props: PageContentProps) {
94124
}
95125

96126
return newContent;
97-
}, [splitMdContent, chatHistories]);
98-
99-
// SSR用のローカルstate
100-
const [dynamicMdContent, setDynamicMdContent] = useState<
101-
DynamicMarkdownSection[]
102-
>(() => initDynamicMdContent());
127+
}, [splitMdContent, chatHistories, sectionInView]);
103128

104129
useEffect(() => {
105130
// props.splitMdContentが変わったとき, チャットのdiffが変わった時に
106-
// ローカルstateとcontextの両方を更新
107-
const newContent = initDynamicMdContent();
108-
setDynamicMdContent(newContent);
109-
setSidebarMdContent(path, newContent);
110-
}, [initDynamicMdContent, path, setSidebarMdContent]);
111-
112-
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
113-
// sectionRefsの長さをsplitMdContentに合わせる
114-
while (sectionRefs.current.length < props.splitMdContent.length) {
115-
sectionRefs.current.push(null);
116-
}
117-
118-
useEffect(() => {
119-
const handleScroll = () => {
120-
const updateContent = (
121-
prevDynamicMdContent: DynamicMarkdownSection[]
122-
) => {
123-
const dynMdContent = prevDynamicMdContent.slice(); // Reactの変更検知のために新しい配列を作成
124-
for (let i = 0; i < sectionRefs.current.length; i++) {
125-
if (sectionRefs.current.at(i) && dynMdContent.at(i)) {
126-
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
127-
dynMdContent.at(i)!.inView =
128-
rect.top < window.innerHeight * 0.9 &&
129-
rect.bottom >= window.innerHeight * 0.1;
130-
}
131-
}
132-
return dynMdContent;
133-
};
134-
135-
// ローカルstateとcontextの両方を更新
136-
setDynamicMdContent(updateContent);
137-
setSidebarMdContent(path, updateContent);
138-
};
139-
window.addEventListener("scroll", handleScroll);
140-
handleScroll();
141-
return () => {
142-
window.removeEventListener("scroll", handleScroll);
143-
};
144-
}, [setSidebarMdContent, path]);
131+
// sidebarのcontextを更新
132+
setSidebarMdContent(path, dynamicMdContent);
133+
}, [dynamicMdContent, path, setSidebarMdContent]);
145134

146135
const [isFormVisible, setIsFormVisible] = useState(false);
147136

app/lib/clientOnly.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use client";
2+
3+
import { useSyncExternalStore } from "react";
4+
5+
// --- SSR無効化のためのカスタムフック準備 ---
6+
const subscribe = () => () => {};
7+
const getSnapshot = () => true; // クライアントでは true
8+
const getServerSnapshot = () => false; // サーバーでは false
9+
10+
export function useIsClient() {
11+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
12+
}

app/markdown/styledSyntaxHighlighter.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
tomorrow,
66
tomorrowNight,
77
} from "react-syntax-highlighter/dist/esm/styles/hljs";
8-
import { lazy, Suspense, useEffect, useState } from "react";
8+
import { lazy, Suspense } from "react";
99
import { LangConstants } from "@my-code/runtime/languages";
1010
import clsx from "clsx";
11+
import { useIsClient } from "@/lib/clientOnly";
1112

1213
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
1314
const SyntaxHighlighter = lazy(() => {
@@ -24,11 +25,8 @@ export function StyledSyntaxHighlighter(props: {
2425
}) {
2526
const theme = useChangeTheme();
2627
const codetheme = theme === "tomorrow" ? tomorrow : tomorrowNight;
27-
const [initHighlighter, setInitHighlighter] = useState(false);
28-
useEffect(() => {
29-
setInitHighlighter(true);
30-
}, []);
31-
return initHighlighter ? (
28+
const isClient = useIsClient();
29+
return isClient ? (
3230
<Suspense fallback={<FallbackPre>{props.children}</FallbackPre>}>
3331
<SyntaxHighlighter
3432
language={props.language.rsh}

app/sidebar.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
ReactNode,
1616
useCallback,
1717
useContext,
18-
useEffect,
1918
useState,
2019
} from "react";
2120
import clsx from "clsx";
@@ -100,22 +99,17 @@ export function Sidebar({ pagesList }: { pagesList: LanguageEntry[] }) {
10099

101100
// 目次の開閉状態
102101
const [detailsOpen, setDetailsOpen] = useState<boolean[]>([]);
103-
const currentLangIndex = pagesList.findIndex(
104-
(group) => currentLang === group.id
105-
);
106-
useEffect(() => {
107-
// 表示しているグループが変わったときに現在のグループのdetailsを開く
108-
if (currentLangIndex !== -1) {
109-
setDetailsOpen((detailsOpen) => {
110-
const newDetailsOpen = [...detailsOpen];
111-
while (newDetailsOpen.length <= currentLangIndex) {
112-
newDetailsOpen.push(false);
113-
}
114-
newDetailsOpen[currentLangIndex] = true;
115-
return newDetailsOpen;
116-
});
117-
}
118-
}, [currentLangIndex]);
102+
const [prevLangIndex, setPrevLangIndex] = useState<number>(-1);
103+
const langIndex = pagesList.findIndex((group) => currentLang === group.id);
104+
// 表示しているグループが変わったときに現在のグループのdetailsを開く
105+
if (prevLangIndex !== langIndex) {
106+
setPrevLangIndex(langIndex);
107+
setDetailsOpen((detailsOpen) => {
108+
const newDetailsOpen = [...detailsOpen];
109+
newDetailsOpen[langIndex] = true;
110+
return newDetailsOpen;
111+
});
112+
}
119113

120114
return (
121115
<div className="bg-base-200 h-full w-sidebar flex flex-col relative">

app/terminal/embedContext.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
ReactNode,
88
useCallback,
99
useContext,
10-
useEffect,
1110
useState,
1211
} from "react";
1312

@@ -58,7 +57,7 @@ export function useEmbedContext() {
5857
export function EmbedContextProvider({ children }: { children: ReactNode }) {
5958
const pathname = usePathname();
6059

61-
const [currentPathname, setCurrentPathname] = useState<PagePathname>("");
60+
const [prevPathname, setPrevPathname] = useState<PagePathname>("");
6261
const [files, setFiles] = useState<
6362
Record<PagePathname, Record<Filename, string>>
6463
>({});
@@ -72,15 +71,12 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
7271
const [execResults, setExecResults] = useState<
7372
Record<Filename, ReplOutput[]>
7473
>({});
75-
// pathnameが変わったらデータを初期化
76-
useEffect(() => {
77-
if (pathname && pathname !== currentPathname) {
78-
setCurrentPathname(pathname);
79-
setReplOutputs({});
80-
setCommandIdCounters({});
81-
setExecResults({});
82-
}
83-
}, [pathname, currentPathname]);
74+
if (pathname && pathname !== prevPathname) {
75+
setPrevPathname(pathname);
76+
setReplOutputs({});
77+
setCommandIdCounters({});
78+
setExecResults({});
79+
}
8480

8581
const writeFile = useCallback(
8682
(updatedFiles: Record<Filename, string>) => {

app/terminal/exec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export function ExecFile(props: ExecProps) {
7777
>("idle");
7878
useEffect(() => {
7979
if (executionState === "triggered" && ready) {
80-
setExecutionState("executing");
8180
(async () => {
81+
setExecutionState("executing");
8282
clearTerminal(terminalInstanceRef.current!);
8383
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
8484
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる

eslint.config.mjs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ const eslintConfig = [
66
{
77
ignores: [
88
".next/**",
9+
".open-next/**",
10+
".wrangler/**",
911
"node_modules/**",
1012
"public/**",
1113
"cloudflare-env.d.ts",
12-
"packages/runtime/node_modules/**",
13-
"packages/runtime/dist/**",
1414
],
1515
},
1616
...nextCoreWebVitals,
@@ -27,10 +27,6 @@ const eslintConfig = [
2727
ignoreRestSiblings: true,
2828
},
2929
],
30-
// react-hooks/refs と react-hooks/set-state-in-effect は Next.js 16 で追加された新しいルールで、
31-
// 既存のコードパターンと相性が悪いため無効化する
32-
"react-hooks/refs": "off",
33-
"react-hooks/set-state-in-effect": "off",
3430
},
3531
},
3632
];

0 commit comments

Comments
 (0)