React页面刷新后无法保留当前主题的问题求助
问题
我用React搭建编程教程网站,内容页包含侧边栏和带「上一页」「下一页」的主内容区用于切换主题。但查看特定主题(比如第10个)后刷新页面,会重置到第一个主题,无法停留在当前主题。我尝试把currentHeadingId和currentSubHeadingId存在localStorage里,但问题依旧。怎么确保页面刷新后加载正确的主题?
附上相关代码:
export const CourseSection = () => { const [isSidebarOpen, setSidebarOpen] = useState(false); const toggleSidebar = () => { setSidebarOpen((prev) => !prev); }; const params = useParams(); const { data: AllSubject } = useGetAllSubjectsQuery(); const { data: allSubheading } = useGetAllSubjectSubHeadingQuery(); const { data: allTopics } = useGetAllTopicHeadingsQuery(); const { data: allTopicContent } = useGetAllTopicContentsQuery(); const { data: allTopicHeadingImage } = useGetTopicImagesQuery(); const { data: allTopicCodeSnippet } = useGetAllTopicCodeSnippetQuery(); const { data: allTopicNotes } = useGetAllNotesQuery(); const [headings, setHeadings] = useState([]); const [currentHeadingId, setCurrentHeadingId] = useState(localStorage.getItem("currentHeadingId") || null); const [currentSubHeadingId, setCurrentSubHeadingId] = useState(localStorage.getItem("currentSubHeadingId") || null); useEffect(() => { if (AllSubject && allSubheading && allTopics && AllSubject.data && allSubheading.data && allTopics.data) { const processedHeadings = AllSubject && AllSubject.data.filter((sub) => sub.id == params.tid).map((subject) => ({ ...subject, headings: allSubheading.data.filter((heading) => heading.subjectId === subject.id).map((heading) => ({ ...heading, subHeadings: allTopics.data.filter((topic) => topic.subHeadingId === heading.id), })), })); setHeadings(processedHeadings); if (processedHeadings.length > 0 && processedHeadings[0].headings.length > 0) { setCurrentHeadingId(processedHeadings[0].headings[0].id); localStorage.setItem("currentHeadingId", processedHeadings[0].headings[0].id) setCurrentSubHeadingId(processedHeadings[0].headings[0].subHeadings[0].id); localStorage.setItem("currentSubHeadingId", processedHeadings[0].headings[0].subHeadings[0].id) } else { setCurrentHeadingId(localStorage.getItem("currentHeadingId")) setCurrentSubHeadingId(localStorage.getItem("currentSubHeadingId")) } } }, [AllSubject, allSubheading, allTopics]); const dispatch = useDispatch(); const course_selector = useSelector(state => state.course.all_contents); useEffect(() => { dispatch(get_all_contents({ topic_id: params.cid, AllSubject, allTopics, allTopicContent, allTopicHeadingImage, allTopicCodeSnippet, allTopicNotes })) }, [params, AllSubject, allTopics, allTopicContent, allTopicHeadingImage, allTopicCodeSnippet, allTopicNotes]); const getContentForCurrentSubHeading = () => { const currentAnswer = allTopicContent && allTopicContent.data?.find((ans) => ans.topicHeadingId === currentSubHeadingId); const currentPhoto = allTopicHeadingImage && allTopicHeadingImage.data?.find((img) => img.topicHeadingId === currentSubHeadingId); const currentCodeSnippet = allTopicCodeSnippet && allTopicCodeSnippet.data?.find((cs) => cs.topicHeadingId === currentSubHeadingId); const currentNotes = allTopicNotes && allTopicNotes.data?.find((note) => note.topicHeadingId === currentSubHeadingId); const currentSubHeading = allTopics && allTopics.data?.find((sub) => sub.id === currentSubHeadingId); const content = course_selector && course_selector.filter((content) => content.topicHeadingId === currentSubHeadingId) return { title: currentSubHeading ? currentSubHeading.topicHeadingName : '', contents: content }; }; const selectSubHeading = (headingId, subHeadingId) => { setCurrentHeadingId(headingId); setCurrentSubHeadingId(subHeadingId); }; const handlePrev = () => { const currentHeading = headings.flatMap(sub => sub.headings).find((h) => h.id === currentHeadingId); const subHeadings = currentHeading ? currentHeading.subHeadings : []; const currentIndex = subHeadings.findIndex((sub) => sub.id === currentSubHeadingId); if (currentIndex > 0) { setCurrentSubHeadingId(subHeadings[currentIndex - 1].id); } else { const prevHeadingIndex = headings.flatMap(sub => sub.headings).findIndex((h) => h.id === currentHeadingId) - 1; if (prevHeadingIndex >= 0) { const prevHeading = headings.flatMap(sub => sub.headings)[prevHeadingIndex]; setCurrentHeadingId(prevHeading.id); setCurrentSubHeadingId(prevHeading.subHeadings[prevHeading.subHeadings.length - 1].id); } } }; const handleNext = () => { const currentHeading = headings.flatMap(sub => sub.headings).find((h) => h.id === currentHeadingId); const subHeadings = currentHeading ? currentHeading.subHeadings : []; const currentIndex = subHeadings.findIndex((sub) => sub.id === currentSubHeadingId); if (currentIndex < subHeadings.length - 1) { setCurrentSubHeadingId(subHeadings[currentIndex + 1].id); } else { const nextHeadingIndex = headings.flatMap(sub => sub.headings).findIndex((h) => h.id === currentHeadingId) + 1; if (nextHeadingIndex < headings.flatMap(sub => sub.headings).length) { const nextHeading = headings.flatMap(sub => sub.headings)[nextHeadingIndex]; setCurrentHeadingId(nextHeading.id); setCurrentSubHeadingId(nextHeading.subHeadings[0].id); } } }; const content = getContentForCurrentSubHeading(); // console.log(content) return ( <div className="flex h-screen"> <Sidebar headings={headings && headings.length > 0 && headings} selectSubHeading={selectSubHeading} /> <MainContent content={content} handlePrev={handlePrev} handleNext={handleNext} /> </div> ); };
问题根源
核心问题出在处理数据加载的useEffect里:每次后端数据加载完成后,你都会无条件把currentHeadingId和currentSubHeadingId设置成第一个主题的ID,直接覆盖了localStorage里保存的值。不管用户之前有没有存储过当前主题,数据一加载就重置,刷新自然回到第一个主题。
另外,你在切换主题(selectSubHeading、handlePrev、handleNext)的时候,只更新了状态但没把新的ID同步到localStorage,这也会导致刷新后读取不到正确的值。
解决方案
1. 修改数据加载的useEffect逻辑
只有当localStorage里没有保存任何ID时,才设置默认的第一个主题;如果有存储值,就用存储的ID,不要覆盖:
useEffect(() => { if (AllSubject && allSubheading && allTopics && AllSubject.data && allSubheading.data && allTopics.data) { const processedHeadings = AllSubject.data.filter((sub) => sub.id == params.tid).map((subject) => ({ ...subject, headings: allSubheading.data.filter((heading) => heading.subjectId === subject.id).map((heading) => ({ ...heading, subHeadings: allTopics.data.filter((topic) => topic.subHeadingId === heading.id), })), })); setHeadings(processedHeadings); // 先从localStorage读取值 const savedHeadingId = localStorage.getItem("currentHeadingId"); const savedSubHeadingId = localStorage.getItem("currentSubHeadingId"); if (processedHeadings.length > 0 && processedHeadings[0].headings.length > 0) { // 如果有存储值,用存储值;没有才用默认第一个 setCurrentHeadingId(savedHeadingId || processedHeadings[0].headings[0].id); setCurrentSubHeadingId(savedSubHeadingId || processedHeadings[0].headings[0].subHeadings[0].id); // 同步到localStorage(如果之前没存储的话) if (!savedHeadingId) { localStorage.setItem("currentHeadingId", processedHeadings[0].headings[0].id); } if (!savedSubHeadingId) { localStorage.setItem("currentSubHeadingId", processedHeadings[0].headings[0].subHeadings[0].id); } } else { setCurrentHeadingId(savedHeadingId); setCurrentSubHeadingId(savedSubHeadingId); } } }, [AllSubject, allSubheading, allTopics]);
2. 切换主题时同步到localStorage
修改selectSubHeading、handlePrev、handleNext方法,每次更新状态时都把新ID存到localStorage:
const selectSubHeading = (headingId, subHeadingId) => { setCurrentHeadingId(headingId); setCurrentSubHeadingId(subHeadingId); localStorage.setItem("currentHeadingId", headingId); localStorage.setItem("currentSubHeadingId", subHeadingId); }; const handlePrev = () => { const currentHeading = headings.flatMap(sub => sub.headings).find((h) => h.id === currentHeadingId); const subHeadings = currentHeading ? currentHeading.subHeadings : []; const currentIndex = subHeadings.findIndex((sub) => sub.id === currentSubHeadingId); if (currentIndex > 0) { const newSubId = subHeadings[currentIndex - 1].id; setCurrentSubHeadingId(newSubId); localStorage.setItem("currentSubHeadingId", newSubId); } else { const prevHeadingIndex = headings.flatMap(sub => sub.headings).findIndex((h) => h.id === currentHeadingId) - 1; if (prevHeadingIndex >= 0) { const prevHeading = headings.flatMap(sub => sub.headings)[prevHeadingIndex]; const newHeadingId = prevHeading.id; const newSubId = prevHeading.subHeadings[prevHeading.subHeadings.length - 1].id; setCurrentHeadingId(newHeadingId); setCurrentSubHeadingId(newSubId); localStorage.setItem("currentHeadingId", newHeadingId); localStorage.setItem("currentSubHeadingId", newSubId); } } }; const handleNext = () => { const currentHeading = headings.flatMap(sub => sub.headings).find((h) => h.id === currentHeadingId); const subHeadings = currentHeading ? currentHeading.subHeadings : []; const currentIndex = subHeadings.findIndex((sub) => sub.id === currentSubHeadingId); if (currentIndex < subHeadings.length - 1) { const newSubId = subHeadings[currentIndex + 1].id; setCurrentSubHeadingId(newSubId); localStorage.setItem("currentSubHeadingId", newSubId); } else { const nextHeadingIndex = headings.flatMap(sub => sub.headings).findIndex((h) => h.id === currentHeadingId) + 1; if (nextHeadingIndex < headings.flatMap(sub => sub.headings).length) { const nextHeading = headings.flatMap(sub => sub.headings)[nextHeadingIndex]; const newHeadingId = nextHeading.id; const newSubId = nextHeading.subHeadings[0].id; setCurrentHeadingId(newHeadingId); setCurrentSubHeadingId(newSubId); localStorage.setItem("currentHeadingId", newHeadingId); localStorage.setItem("currentSubHeadingId", newSubId); } } };
3. 可选:用URL参数持久化状态(更友好的方案)
如果希望用户能直接分享当前主题的链接,还可以把currentHeadingId和currentSubHeadingId放到URL参数里,比如/course/:tid/:headingId/:subHeadingId。这样刷新页面时直接从useParams读取,同时更新localStorage,体验会更好。
比如修改selectSubHeading时用navigate更新URL:
import { useNavigate } from 'react-router-dom'; // 组件内初始化 const navigate = useNavigate(); const selectSubHeading = (headingId, subHeadingId) => { setCurrentHeadingId(headingId); setCurrentSubHeadingId(subHeadingId); localStorage.setItem("currentHeadingId", headingId); localStorage.setItem("currentSubHeadingId", subHeadingId); // 更新URL参数 navigate(`/course/${params.tid}/${headingId}/${subHeadingId}`); };
然后在useEffect里优先从URL参数读取,再回退到localStorage和默认值。
内容的提问来源于stack exchange,提问作者SAYANTAN HALDAR

