使用 createRef 解决嵌套导航 + IntersectionObserver 的问题
React issues with createRef for nested navigation + IntersectionObserver
我有一个 intersectionObserver,它监视某些部分并突出显示相应的导航项。但我只设法让“主要部分 Microsoft, Amazon
工作,但不是小节 Define, Branding, Design, Deduction
。如下面的 gif 所示:
我希望它以这种方式构建的原因是,如果子部分在视图中,我可以突出显示“主要”部分。
半工作演示:https://codesandbox.io/s/intersection-with-hooks-fri5jun1344-fe03x
似乎我也可以复制和粘贴与小节相同的功能。但是我很难思考如何处理嵌套数据 + useRef + reducer。我想知道是否有人可以给我一个正确方向的指示。
这是所需效果的 gif 图像。请注意,如果其中一个小节在视图中,主标题 (Loupe, Canon) 仍会突出显示:
一切都从一个数据数组开始
const data = [
{
title: "Microsoft",
id: "microsoft",
color: "#fcf6f5",
year: "2020",
sections: ["define", "branding", "design", "deduction"]
},
{
title: "Amazon",
id: "amazon",
color: "#FFE2DD",
year: "2018",
sections: ["define", "design", "develop", "deduction"]
},
{
title: "Apple",
id: "apple",
color: "#000",
year: "2020",
sections: ["about", "process", "deduction"]
}
];
App.js padding data
object 到 reduce 以创建 Refs
const refs = data.reduce((refsObj, Case) => {
refsObj[Case.id] = React.createRef();
return refsObj;
}, {});
我的组件传入道具
<Navigation
data={data}
handleClick={handleClick}
activeCase={activeCase}
/>
{data.map(item => (
<Case
key={item.id}
activeCase={activeCase}
setActiveCase={setActiveCase}
refs={refs}
data={item}
/>
))}
Case.js
export function Case({ data, refs, activeCase, setActiveCase }) {
const components = {
amazon: Amazon,
apple: Apple,
microsoft: Microsoft
};
class DefaultError extends Component {
render() {
return <div>Error, no page found</div>;
}
}
const Tag = components[data.id] || DefaultError;
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target.id !== activeCase && entry.isIntersecting) {
setActiveCase(entry.target.id);
}
});
}, observerConfig);
observer.observe(refs[data.id].current);
return () => observer.disconnect(); // Clenaup the observer if unmount
}, [activeCase, setActiveCase, refs, data]);
return (
<React.Fragment>
<section
ref={refs[data.id]}
id={data.id}
className="section"
style={{ marginBottom: 400 }}
>
<Tag data={data} />
</section>
</React.Fragment>
);
}
我试过像这样映射小节,但我卡在了这一部分:
const subRefs = data.map((refsObj, Case) => {
refsObj[Case] = React.createRef();
return refsObj;
}, {});
您可以简化代码。对于您的用例,您实际上并不需要 refs
或 intersectionObservers
。您可以简单地 scrollIntoView
使用 document.getElementById
(您已经有了导航的 ID。
你可以在 handleClick
中做得很好 setActiveCase
。
像这样修改handleClick
const handleClick = (subTabId, mainTabName) => {
//console.log("subTabName, mainTabName", subTabId, mainTabName);
setActiveCase({ mainTab: mainTabName, subTab: subTabId.split("--")[1] }); //use this for active tab styling etc
document.getElementById(subTabId).scrollIntoView({
behavior: "smooth",
block: "start"
});
};
Navigation.js 像这样调用 handleClick。
{item.sections &&
item.sections.map(subItem => (
<div
className={`${styles.anchor}`}
key={`#${item.title}--${subItem}`}
sx={{ marginRight: 3, fontSize: 0, color: "text" }}
href={`#${item.title}--${subItem}`}
onClick={e => {
handleClick(`${item.id}--${subItem}`, item.id);
e.stopPropagation();
}}
>
{toTitleCase(subItem)}
</div>
))}
我在尝试保持您的大部分逻辑完整的同时找到了解决方案。首先,您需要做的是将 subrefs(sections ref)存储在与您的 Case ref 相同的对象中。所以你需要一个额外的 reduce 函数来创建 refs 对象中的那些:
App.js
const refs = data.reduce((refsObj, Case) => { // Put this outside the render
const subRefs = Case.sections.reduce((subrefsObj, Section) => {
subrefsObj[Section] = React.createRef();
return subrefsObj;
}, {});
refsObj[Case.id] = {
self: React.createRef(), // self is the Case ref, like Apple, Microsoft...
subRefs // This is going to be the subrefs
};
return refsObj;
}, {});
然后你添加一个额外的状态来处理哪个子部分是活动的,比如 const [activeSection, setActiveSection] = React.useState();
然后你把它放在任何你也使用 activeCase
的地方。您需要它,因为您说过 Case 和 Sections 需要独立工作。 (两者同时激活)。
Case.js
您需要将子引用传递给子组件,因此您可以:
<Tag data={data} subRefs={refs[data.id].subRefs} />
并且您还需要每个子引用的交集观察器。所以你的 useEffect 看起来像:
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observerCallback = (entries, isCase) => {
const activeEntry = entries.find(entry => entry.isIntersecting);
if (activeEntry) {
if (isCase) setActiveCase(activeEntry.target.id);
else setActiveSection(activeEntry.target.id);
} else if (isCase) {
setActiveCase(null);
setActiveSection(null);
}
};
const caseObserver = new IntersectionObserver(
entries => observerCallback(entries, true),
observerConfig
);
caseObserver.observe(refs[data.id].self.current);
const sectionObserver = new IntersectionObserver(
entries => observerCallback(entries, false),
observerConfig
);
Object.values(refs[data.id].subRefs).forEach(subRef => {
sectionObserver.observe(subRef.current);
});
return () => {
caseObserver.disconnect();
sectionObserver.disconnect();
}; // Clenaup the observer if unmount
}, [refs, data]);
然后在你的 amazon/index.js ,microsoft/index.js 和 apple/index.js 个文件。您再次传递参考:
<Template
data={this.props.data}
caseSections={caseSections}
subRefs={this.props.subRefs}
/>
最后,在您的 template.js 文件中,您将拥有以下内容,以便您可以分配正确的参考:
const Template = props => {
return (
<React.Fragment>
<div
sx={{
background: "#eee",
transition: "background ease 0.5s"
}}
>
{props.data.sections &&
props.data.sections.map(subItem => (
<Container
ref={props.subRefs && props.subRefs[subItem]}
id={`${props.data.id}--${subItem}`}
key={subItem}
className="article"
>
<Section sectionId={subItem} caseSections={props.caseSections} />
</Container>
))}
</div>
</React.Fragment>
);
};
我相信大部分内容都包含在 post 中。您可以查看您的 forked working repo here
我有一个 intersectionObserver,它监视某些部分并突出显示相应的导航项。但我只设法让“主要部分 Microsoft, Amazon
工作,但不是小节 Define, Branding, Design, Deduction
。如下面的 gif 所示:
我希望它以这种方式构建的原因是,如果子部分在视图中,我可以突出显示“主要”部分。
半工作演示:https://codesandbox.io/s/intersection-with-hooks-fri5jun1344-fe03x
似乎我也可以复制和粘贴与小节相同的功能。但是我很难思考如何处理嵌套数据 + useRef + reducer。我想知道是否有人可以给我一个正确方向的指示。
这是所需效果的 gif 图像。请注意,如果其中一个小节在视图中,主标题 (Loupe, Canon) 仍会突出显示:
一切都从一个数据数组开始
const data = [
{
title: "Microsoft",
id: "microsoft",
color: "#fcf6f5",
year: "2020",
sections: ["define", "branding", "design", "deduction"]
},
{
title: "Amazon",
id: "amazon",
color: "#FFE2DD",
year: "2018",
sections: ["define", "design", "develop", "deduction"]
},
{
title: "Apple",
id: "apple",
color: "#000",
year: "2020",
sections: ["about", "process", "deduction"]
}
];
App.js padding data
object 到 reduce 以创建 Refs
const refs = data.reduce((refsObj, Case) => {
refsObj[Case.id] = React.createRef();
return refsObj;
}, {});
我的组件传入道具
<Navigation
data={data}
handleClick={handleClick}
activeCase={activeCase}
/>
{data.map(item => (
<Case
key={item.id}
activeCase={activeCase}
setActiveCase={setActiveCase}
refs={refs}
data={item}
/>
))}
Case.js
export function Case({ data, refs, activeCase, setActiveCase }) {
const components = {
amazon: Amazon,
apple: Apple,
microsoft: Microsoft
};
class DefaultError extends Component {
render() {
return <div>Error, no page found</div>;
}
}
const Tag = components[data.id] || DefaultError;
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target.id !== activeCase && entry.isIntersecting) {
setActiveCase(entry.target.id);
}
});
}, observerConfig);
observer.observe(refs[data.id].current);
return () => observer.disconnect(); // Clenaup the observer if unmount
}, [activeCase, setActiveCase, refs, data]);
return (
<React.Fragment>
<section
ref={refs[data.id]}
id={data.id}
className="section"
style={{ marginBottom: 400 }}
>
<Tag data={data} />
</section>
</React.Fragment>
);
}
我试过像这样映射小节,但我卡在了这一部分:
const subRefs = data.map((refsObj, Case) => {
refsObj[Case] = React.createRef();
return refsObj;
}, {});
您可以简化代码。对于您的用例,您实际上并不需要 refs
或 intersectionObservers
。您可以简单地 scrollIntoView
使用 document.getElementById
(您已经有了导航的 ID。
你可以在 handleClick
中做得很好 setActiveCase
。
像这样修改handleClick
const handleClick = (subTabId, mainTabName) => {
//console.log("subTabName, mainTabName", subTabId, mainTabName);
setActiveCase({ mainTab: mainTabName, subTab: subTabId.split("--")[1] }); //use this for active tab styling etc
document.getElementById(subTabId).scrollIntoView({
behavior: "smooth",
block: "start"
});
};
Navigation.js 像这样调用 handleClick。
{item.sections &&
item.sections.map(subItem => (
<div
className={`${styles.anchor}`}
key={`#${item.title}--${subItem}`}
sx={{ marginRight: 3, fontSize: 0, color: "text" }}
href={`#${item.title}--${subItem}`}
onClick={e => {
handleClick(`${item.id}--${subItem}`, item.id);
e.stopPropagation();
}}
>
{toTitleCase(subItem)}
</div>
))}
我在尝试保持您的大部分逻辑完整的同时找到了解决方案。首先,您需要做的是将 subrefs(sections ref)存储在与您的 Case ref 相同的对象中。所以你需要一个额外的 reduce 函数来创建 refs 对象中的那些:
App.js
const refs = data.reduce((refsObj, Case) => { // Put this outside the render
const subRefs = Case.sections.reduce((subrefsObj, Section) => {
subrefsObj[Section] = React.createRef();
return subrefsObj;
}, {});
refsObj[Case.id] = {
self: React.createRef(), // self is the Case ref, like Apple, Microsoft...
subRefs // This is going to be the subrefs
};
return refsObj;
}, {});
然后你添加一个额外的状态来处理哪个子部分是活动的,比如 const [activeSection, setActiveSection] = React.useState();
然后你把它放在任何你也使用 activeCase
的地方。您需要它,因为您说过 Case 和 Sections 需要独立工作。 (两者同时激活)。
Case.js
您需要将子引用传递给子组件,因此您可以:
<Tag data={data} subRefs={refs[data.id].subRefs} />
并且您还需要每个子引用的交集观察器。所以你的 useEffect 看起来像:
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observerCallback = (entries, isCase) => {
const activeEntry = entries.find(entry => entry.isIntersecting);
if (activeEntry) {
if (isCase) setActiveCase(activeEntry.target.id);
else setActiveSection(activeEntry.target.id);
} else if (isCase) {
setActiveCase(null);
setActiveSection(null);
}
};
const caseObserver = new IntersectionObserver(
entries => observerCallback(entries, true),
observerConfig
);
caseObserver.observe(refs[data.id].self.current);
const sectionObserver = new IntersectionObserver(
entries => observerCallback(entries, false),
observerConfig
);
Object.values(refs[data.id].subRefs).forEach(subRef => {
sectionObserver.observe(subRef.current);
});
return () => {
caseObserver.disconnect();
sectionObserver.disconnect();
}; // Clenaup the observer if unmount
}, [refs, data]);
然后在你的 amazon/index.js ,microsoft/index.js 和 apple/index.js 个文件。您再次传递参考:
<Template
data={this.props.data}
caseSections={caseSections}
subRefs={this.props.subRefs}
/>
最后,在您的 template.js 文件中,您将拥有以下内容,以便您可以分配正确的参考:
const Template = props => {
return (
<React.Fragment>
<div
sx={{
background: "#eee",
transition: "background ease 0.5s"
}}
>
{props.data.sections &&
props.data.sections.map(subItem => (
<Container
ref={props.subRefs && props.subRefs[subItem]}
id={`${props.data.id}--${subItem}`}
key={subItem}
className="article"
>
<Section sectionId={subItem} caseSections={props.caseSections} />
</Container>
))}
</div>
</React.Fragment>
);
};
我相信大部分内容都包含在 post 中。您可以查看您的 forked working repo here