【问题标题】:Is it a misuse of context to use it to store JSX components for display elsewhere?使用它来存储 JSX 组件以在其他地方显示是否是滥用上下文?
【发布时间】:2020-07-21 14:38:50
【问题描述】:

我有一个应用程序,用户将在其中单击应用程序的各个部分,这将在右侧的抽屉中显示某种配置选项。

我为此得到的解决方案是将要显示的任何内容存储在上下文中。这样,抽屉只需要从上下文中检索其内容,并且需要设置内容的任何部分都可以直接通过上下文进行设置。

这是CodeSandbox demonstrating this

键码sn-ps:

const MainContent = () => {
  const items = ["foo", "bar", "biz"];

  const { setContent } = useContext(DrawerContentContext);
  /**
   * Note that in the real world, these components could exist at any level of nesting 
   */
  return (
    <Fragment>
      {items.map((v, i) => (
        <Button
          key={i}
          onClick={() => {
            setContent(<div>{v}</div>);
          }}
        >
          {v}
        </Button>
      ))}
    </Fragment>
  );
};

const MyDrawer = () => {
  const classes = useStyles();

  const { content } = useContext(DrawerContentContext);

  return (
    <Drawer
      anchor="right"
      open={true}
      variant="persistent"
      classes={{ paper: classes.drawer }}
    >
      draw content
      <hr />
      {content ? content : "empty"}
    </Drawer>
  );
};

export default function SimplePopover() {
  const [drawContent, setDrawerContent] = React.useState(null);
  return (
    <div>
      <DrawerContentContext.Provider
        value={{
          content: drawContent,
          setContent: setDrawerContent
        }}
      >
        <MainContent />
        <MyDrawer />
      </DrawerContentContext.Provider>
    </div>
  );
}

我的问题是 - 这是对上下文的适当使用,还是这种解决方案可能会遇到渲染/虚拟 dom 等问题?

有没有更整洁的方法来做到这一点? (即自定义钩子?但请记住,一些想要进行设置的组件可能不是功能组件)。

【问题讨论】:

  • 为什么不能将不同的值存储为上下文数据并根据这些值有条件地在 Drawer 组件中呈现 UI?
  • @rzwnahmd - 因为实际上我想放在抽屉里的东西不仅仅是“数据”。它们可能是具有自定义交互等的不同形式。

标签: reactjs react-context


【解决方案1】:

请注意,纯粹从技术角度将组件存储在 Context 中是可以的,因为 JSX 结构只不过是最终使用 React.createElement 编译的对象。

但是,您尝试实现的目标可以通过门户轻松完成,并且在您通过上下文渲染后,您可以更好地控制在其他地方渲染的组件,因为如果您直接渲染它们,您可以更好地控制状态和处理程序而不是将它们存储为上下文中的值

将组件存储在上下文中并渲染它们的另一个缺点是它使组件的调试变得非常困难。如果组件变得复杂,您通常会发现很难确定 props 的供应商是谁

import React, {
  Fragment,
  useContext,
  useState,
  useRef,
  useEffect
} from "react";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import { Drawer } from "@material-ui/core";
import ReactDOM from "react-dom";

const useStyles = makeStyles(theme => ({
  typography: {
    padding: theme.spacing(2)
  },
  drawer: {
    width: 200
  }
}));

const DrawerContentContext = React.createContext({
  ref: null,
  setRef: () => {}
});

const MainContent = () => {
  const items = ["foo", "bar", "biz"];
  const [renderValue, setRenderValue] = useState("");
  const { ref } = useContext(DrawerContentContext);

  return (
    <Fragment>
      {items.map((v, i) => (
        <Button
          key={i}
          onClick={() => {
            setRenderValue(v);
          }}
        >
          {v}
        </Button>
      ))}
      {renderValue
        ? ReactDOM.createPortal(<div>{renderValue}</div>, ref.current)
        : null}
    </Fragment>
  );
};

const MyDrawer = () => {
  const classes = useStyles();
  const contentRef = useRef(null);
  const { setRef } = useContext(DrawerContentContext);
  useEffect(() => {
    setRef(contentRef);
  }, []);
  return (
    <Drawer
      anchor="right"
      open={true}
      variant="persistent"
      classes={{ paper: classes.drawer }}
    >
      draw content
      <hr />
      <div ref={contentRef} />
    </Drawer>
  );
};

export default function SimplePopover() {
  const [ref, setRef] = React.useState(null);
  return (
    <div>
      <DrawerContentContext.Provider
        value={{
          ref,
          setRef
        }}
      >
        <MainContent />
        <MyDrawer />
      </DrawerContentContext.Provider>
    </div>
  );
}

CodeSandbox demo

【讨论】:

  • 你可以通过重构你的代码来让上面的代码变得更好,这样你甚至不需要使用上下文来存储 ref 并且可以从父级传递它。您还可以概括使用 ReactDOM.createPortal 的部分
  • 这个例子的问题是多个东西使用 ref 把 contentn 放在那里。 (在这种情况下不会发生,因为 ref 的使用只发生在一个组件中)在某些情况下这可能是可以的,但在很多情况下不是。请参阅此示例了解我的意思:codesandbox.io/s/material-demo-mrmw8?file=/demo.js:2047-2053
  • 这里有更好的例子:codesandbox.io/s/material-demo-w3lc0?file=/demo.js 我这样做的方式可以强制一次只有“TheContent”组件,并使用它来控制活动状态。我很好奇你会怎么做。
【解决方案2】:

关于性能,订阅上下文的组件将在上下文更改时重新呈现,无论它们存储的是任意数据、jsx 元素还是 React 组件。有一个开放的RFC for context selectors 可以解决这个问题,但与此同时,一些解决方法是useContextSelector 和 Redux。

除了性能之外,它是否是误用取决于它是否使代码更易于使用。请记住,React 元素只是对象。 docs 说:

React 元素是普通对象,创建起来很便宜

而 jsx 只是语法。 docs

每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。

所以,如果存储 { children: 'foo', element: 'div' } 没问题,那么在很多情况下 &lt;div&gt;foo&lt;/div&gt; 也是如此。

【讨论】:

    【解决方案3】:

    是的,您可以这样做,因为 React 组件只是对象,您可以将对象存储为上下文值。但是这样做几乎没有问题,现在您将无法使用停止不必要的重新渲染,因为 React 的对象将始终具有不同的引用,因此您的子组件每次都会重新渲染。

    我的建议是在你的上下文中存储简单的值,并在你想要的任何地方有条件地呈现 UI。

    【讨论】:

      【解决方案4】:

      我在我的项目中使用以下逻辑以获得更好的性能和更大的灵活性,您可以将其用于设计页面布局,您也可以按照自己的方式进行开发。在这个方法中,没有使用context,而是使用refuseImperativeHandle

      useImperativeHandle 非常相似,但它可以让你做两件事:

      1. 它使您可以控制返回的值。您无需返回实例元素,而是明确说明返回值将是什么(参见下面的 sn-p)。
      2. 它允许您用您自己的功能替换本机功能(例如模糊、聚焦等),从而允许对正常行为或完全不同的行为产生副作用。不过,您可以随意调用该函数。

      useImperativeHandle 自定义暴露的实例值 父组件使用时ref

      更多信息:

      反应文档:useImperativeHandle

      Stackoverflow 问题:when to use useImperativeHandle ...


      我的示例演示:codesandbox

      我的例子的结构:

      src
       |---pages
       |     |---About.js
       |     |---Home.js
       |
       |---App.js
       |---CustomPage.js
       |---DrawerSidebar.js
       |---index.js
      

      index.js 文件:

      import React from "react";
      import ReactDOM from "react-dom";
      
      import App from "./App";
      
      import { BrowserRouter as Router } from "react-router-dom";
      
      const rootElement = document.getElementById("root");
      ReactDOM.render(
        <Router>
          <App />
        </Router>,
        rootElement
      );
      

      App.js 文件:

      import React from "react";
      import "./styles.css";
      import { Switch, Route, Link } from "react-router-dom";
      import Home from "./pages/Home";
      import About from "./pages/About";
      
      export default function App() {
        return (
          <div className="App">
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/about">About</Link>
              </li>
            </ul>
            <Switch>
              <Route path="/" exact component={Home} />
              <Route path="/about" exact component={About} />
            </Switch>
          </div>
        );
      }
      

      CustomPage.js 文件:

      import React, { useRef, useImperativeHandle } from "react";
      
      import DrawerSidebar from "./DrawerSidebar";
      
      const CustomPage = React.forwardRef((props, ref) => {
        const leftSidebarRef = useRef(null);
        const rootRef = useRef(null);
      
        useImperativeHandle(ref, () => {
          return {
            rootRef: rootRef,
            toggleLeftSidebar: () => {
              leftSidebarRef.current.toggleSidebar();
            }
          };
        });
      
        return (
          <div className="page">
            <h1>custom page with drawer</h1>
            <DrawerSidebar position="left" ref={leftSidebarRef} rootRef={rootRef} />
      
            <div className="content">{props.children}</div>
          </div>
        );
      });
      
      export default React.memo(CustomPage);
      

      DrawerSidebar.js 文件:

      import React, { useState, useImperativeHandle } from "react";
      import { Drawer } from "@material-ui/core";
      
      const DrawerSidebar = (props, ref) => {
        const [isOpen, setIsOpen] = useState(false);
      
        useImperativeHandle(ref, () => ({
          toggleSidebar: handleToggleDrawer
        }));
      
        const handleToggleDrawer = () => {
          setIsOpen(!isOpen);
        };
      
        return (
          <React.Fragment>
            <Drawer
              variant="temporary"
              anchor={props.position}
              open={isOpen}
              onClose={handleToggleDrawer}
              onClick={handleToggleDrawer}
            >
              <ul>
                <li>home</li>
                <li>about</li>
                <li>contact</li>
              </ul>
            </Drawer>
          </React.Fragment>
        );
      };
      
      export default React.forwardRef(DrawerSidebar);
      

      About.js 文件:

      import React, { useRef } from "react";
      import CustomPage from "../CustomPage";
      
      const About = () => {
        const pageRef = useRef(null);
      
        return (
          <div>
            <CustomPage ref={pageRef}>
              <h1>About page</h1>
              <button onClick={() => pageRef.current.toggleLeftSidebar()}>
                open drawer in About page
              </button>
            </CustomPage>
          </div>
        );
      };
      
      export default About;
      

      Home.js 文件:

      import React, { useRef } from "react";
      import CustomPage from "../CustomPage";
      
      const Home = () => {
        const pageRef = useRef(null);
      
        return (
          <div>
            <CustomPage ref={pageRef}>
              <h1>Home page</h1>
              <button onClick={() => pageRef.current.toggleLeftSidebar()}>
                open drawer in Home page
              </button>
            </CustomPage>
          </div>
        );
      };
      
      export default Home;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-01-02
        • 1970-01-01
        • 1970-01-01
        • 2019-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多