使用 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;
   }, {});

您可以简化代码。对于您的用例,您实际上并不需要 refsintersectionObservers。您可以简单地 scrollIntoView 使用 document.getElementById (您已经有了导航的 ID。

你可以在 handleClick 中做得很好 setActiveCase

Working demo

像这样修改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>
            ))}

Working Example

我在尝试保持您的大部分逻辑完整的同时找到了解决方案。首先,您需要做的是将 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.jsapple/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