使用它来存储 JSX 组件以在其他地方显示是不是是滥用上下文?

Posted

技术标签:

【中文标题】使用它来存储 JSX 组件以在其他地方显示是不是是滥用上下文?【英文标题】: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 - 因为实际上我想放在抽屉里的东西不仅仅是“数据”。它们可能是具有自定义交互等的不同形式。 【参考方案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 非常相似,但它可以让你做两件事:

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

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

更多信息:

反应文档:useImperativeHandle

*** 问题: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;

【讨论】:

以上是关于使用它来存储 JSX 组件以在其他地方显示是不是是滥用上下文?的主要内容,如果未能解决你的问题,请参考以下文章

在我的 html 文件中需要 React 组件

如何在我的 ag-grid 单元格中放置一个 react jsx 组件

React.js学习之理解JSX和组件

JSX 的语法高亮显示在 VS Code 中针对非驼峰式组件道具损坏

如何将 React 组件导出为 NPM 包以在单独的项目中使用?

单击其他地方时触发组件方法