【问题标题】:React 17: how to pass an event handler in a Router <LINK> tagReact 17:如何在路由器 <LINK> 标签中传递事件处理程序
【发布时间】:2020-12-27 16:05:42
【问题描述】:

我正在尝试通过 &lt;LINK&gt; 标记传递事件处理程序。我的最终目标是将事件处理程序传递给子组件:

<Post post={post} onClickClose={props.onClickClose} />

因为我使用的是路由器,所以我无法使用标准道具传递事件处理程序。我相信这个问题一定有解决方案,因为这似乎是一个相当正常的功能。

应用说明:

  1. 待办事项列表允许用户删除每个待办事项
  2. 待办事项列表允许用户导航到每个单独的帖子
  3. 每个单独的帖子都允许用户删除自己,使用与待办事项列表有权访问的相同方法引用

应用层次结构:

App
- Header
- - NotFound
- - ToDoList *
- - - Post **
- - Posts
- - - ToDoList *
- - - - ToDo **

** Ref: onClickClose -> removeTodo

* Method: removeTodo

这是我的代码的一个非常简短的版本:

设置:

<!DOCTYPE HTML>
<html>
  <head>
  
    <script src="assets/babel/bbh.js"></script>
    <script src="https://unpkg.com/react/umd/react.development.js" type="text/javascript"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js" type="text/javascript"></script>
    <script src='https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js'></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js" type="text/javascript"></script>
  
  </head>
  <body>
  
    <div id="app">
    
      <script type="text/babel" name="react-example">
      
        import React, { Component } from 'react';
        import { render } from 'react-dom';
        import ReactDOM from 'react-dom';
        import { welcome } from './bbh';
        
        const Link = ReactRouterDOM.Link;
        const NavLink = ReactRouterDOM.NavLink;
        const Route = ReactRouterDOM.Route;
        const Switch = ReactRouterDOM.Switch;
        const Router = ReactRouterDOM.BrowserRouter;
        const Redirect = ReactRouterDOM.Redirect;
        
        // ToDo class component
        
        class ToDo extends React.Component {
          constructor(props) {
            super(props);
          }
          render() {
            const link = "/post/" + this.props.slug;
            return (
              <Link to={
                {
                  pathname: link,
                  state: {
                    onClickClose: this.props.onClickClose
                  }
                }
              }>
                  My Link
              </Link>
            )
          }
        }
        
        /*
          Other class components:

          App
          Post
          Posts
          NotFound
          ToDoList
          
        */
        
        // Header class component
        
        class Header extends React.Component {
          constructor(props) {
            super(props);
            this.state = {
              posts: [
                {
                  id: 1,
                  slug: "hello-react",
                  title: "Hello React",
                  content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
                  done: true,
                  createdAt: dateFns.format(new Date(), "YYYY-MM-DD HH:mm:ss")
                },
                {
                  id: 2,
                  slug: "hello-project",
                  title: "Hello Project",
                  content: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
                  done: true,
                  createdAt: dateFns.format(new Date(), "YYYY-MM-DD HH:mm:ss")
                },
                {
                  id: 3,
                  slug: "hello-blog",
                  title: "Hello Blog",
                  content: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
                  done: true,
                  createdAt: dateFns.format(new Date(), "YYYY-MM-DD HH:mm:ss")
                },
              ]
            };
          }
          render() {
            return (
              <Switch>
                <Route exact path="/" render={() => <Posts posts={this.state.posts} />} />
                <Route path="/post/:postSlug" render={(props) => {
                  console.log("props ",props);
                  const post = this.state.posts.find(
                    (post) => post.slug === props.match.params.postSlug
                  );
                  if (post) return 
                    <Post post={post} onClickClose={props.onClickClose} />
                    // ERROR
                  else return 
                    <NotFound />
                }}
                />
                <Route component={NotFound} />
              </Switch>
            )
          }
        }
        
        render(<App />, document.getElementById('app'));
      
      </script>
    
    </div>
    
    <script>
      bbh.babelConfig = {
        presets: ['es2015', 'react'],
        plugins: ['transform-object-rest-spread'],
        minified: true
      } 
    </script>
  
  </body>
</html>

当我运行代码时,我得到一个错误。

似乎我可以在 LINK -> to.state 中传递任何东西,除了事件处理程序?

我也尝试在 LINK 标记中创建标准道具,例如:

<Link onClickClose={this.props.onClickClose} to={link}>
    My Link
</Link>

但是,onClickClose 属性 没有传递到路由器?

谁能提供在 LINK 标记中传递事件处理程序的解决方案?

环境:

  • Windows 10
  • 反应 17
  • react-router-dom 4.3.1

注意事项:

我使用 react-router-dom 4.3.1 的原因是,这是唯一可以正常工作的 CDN 路由器版本。我正在使用 CDN 版本,因为我试图在 codepen.io

上显示它

更新:

codepen.io

https://codepen.io/charles1971/pen/GRjOVzz

【问题讨论】:

  • 使用上下文来实现它

标签: javascript reactjs react-router


【解决方案1】:

是的,你不能(也不应该!)在 React Router 中将不可序列化的对象传递给 state。在浏览器中,它使用历史 API,它本身要求状态是可序列化的(哪些函数不是)。

如果您试图拦截链接的行为(例如,取消导航到链接),您可以使用onClick 做到这一点。但是,您看起来更像是在尝试指示您的代码在对不同页面执行操作后执行特定操作。

看起来您正在尝试使用链接来模拟导航 - 为什么不只是使用链接和锚点而不是尝试使用 onClick 处理程序?例如,不要将回调作为属性传递,而是传递任务完成后下一页应访问的 URL(或对象)。

回应您的 cmets:

问题是,孩子在路由器的另一边。

简而言之,没有。您没有在这里定义父子关系。至少在您列出的代码中,您已经定义了ToDo,这是一个&lt;Link&gt;,它将链接你到一个孩子。渲染树可以更准确地表示如下:

<Header>
  <Router>
    <Route>
      <Posts>
        <ToDo>
        <ToDo>
        <ToDo>
      </Posts>
    </Route>
    <Route>
      <Post>
    </Route>
  </Router>
</Header>

从上面的代码可以看出,你所拥有的实际上并不是父子关系(尽管组件名义上是相关的),而是兄弟姐妹关系。有几种方法可以实现你想要做的事情。 最简单的方法是在两个路由中都使用链接 - 例如,您可能有一个指向 &lt;Posts&gt; 组件中每个帖子的链接,然后是一个指向 &lt;Posts&gt; 路由的 &lt;Link&gt;每个&lt;Post&gt; 路由:

// In <Posts>
<ul>
  {posts.map(p => <li><Link to={`/posts/${p.id}`} /></li>)}
</ul>

// In <Post>
<div>
  <Link to='/posts'>Back</Link>
   .... Content of the post
</div>

没有必要让这比使用简单的链接复杂得多,这就是用户期望网站无论如何都能工作的方式:)

如果您不想在&lt;Post&gt; 中硬编码索引页面的位置 - 这是可以理解的 - 您可以改用位置状态。但请记住,这会稍微掩盖应用程序的流程,并且不适用于常规锚点(这意味着通过书签或刷新访问您的网站的用户可能会获得稍微不同的体验):

const ToDo = ({ returnURL }) => {
  return <Link to={{ pathname: link, state: { returnURL  }} />
}

const Post = ({ defaultReturnURL }) => {
  const loc = useLocation();
  const returnURL = loc.state.returnURL ?? "/posts";
  return <div>
    <Link to={returnURL}>Back to Posts</Link>
    ....
  </div>  
}

【讨论】:

  • 谢谢丹。感谢您澄清无法通过 React 路由器传递不可序列化对象的事实。我从来不知道这一点。我实际上是在尝试将事件处理程序引用从父级传递给子级。问题是,孩子在路由器的另一边。通常,我可以通过属性和道具将它从父级传递给子级,但是因为我使用链接 [todo 组件:父级] 来访问子级 [post 组件:子级],这是不可能的?
  • 我的意思是我必须使用 React Router 方法。而且我必须使用
  • 我已经更新了我的答案,但根据我所看到的,我认为你不需要传递函数
  • 丹。我正在考虑您的 cmets,但与此同时,我已在 codepen 链接中添加了完整代码。请参阅更新的问题。 ToDo List 允许用户删除每个 ToDo,但是如果您单击链接图标,它会将您带到单个帖子。在这里,您可以使用垃圾桶图标删除帖子。它使用相同的 ToDoList.removeTodo 方法,我需要访问该方法。这就是为什么我需要传递函数引用...
【解决方案2】:

更新的笔:

https://codepen.io/charles1971/pen/GRjOVzz

解决方案:

此问题的解决方案是从两个不同的组件中引用&lt;ToDoList /&gt; 组件两次。

&lt;ToDoList /&gt; 组件包含 removeToDo 方法。不知何故,我们需要能够在两个不同的地方使用这种方法。 &lt;ToDoList /&gt; 组件需要同时被&lt;Posts /&gt; 组件和&lt;Header /&gt; 组件引用。

然后需要向&lt;ToDoList /&gt; 组件添加两个新的道具:

新道具

起源

定义引用组件的来源

id

标识当前活动帖子的链接

之前

<ToDoList posts="" />

之后

<ToDoList posts="" origin="" id="" />

现在,可以利用这些新道具在&lt;ToDoList /&gt; 组件的render 方法中使用三元运算符。

ToDoList 渲染方法

... 表示额外的 HTML 代码

render{
    let todos = null;
    if(this.props.origin == "posts"){
        todos = this.state.todos.map(
          function (todo, index) {
            return (
              <ToDo 
                key={index} 
                keyRef={index} 
                ref={index} 
                title={todo.title} 
                done={todo.done} 
                slug={todo.slug} 
                onClickClose={this.removeTodo.bind(this,index,"posts")}
                onClickDone={this.markTodoDone.bind(this,index)}
                onClickUp={this.move.bind(this,index,"up")}
                onClickDown={this.move.bind(this,index,"down")}
              />
            );
          }.bind(this)
        );
    }
    else{
        todos = this.state.todos.map(
          function (todo, index) {
            return (
              <Post 
                key={index} 
                keyRef={index} 
                ref={index} 
                title={todo.title} 
                done={todo.done} 
                slug={todo.slug} 
                createdAt={todo.createdAt} 
                content={todo.content} 
                id1={this.props.id} 
                id2={todo.id} 
                onClickClose={this.removeTodo.bind(this,index,"post")}
              />
            );
          }.bind(this)
        );
    }
    return (
        this.state.redirect == false ? (
      this.props.origin == "posts" ? 
            (
                ...
                {todos}
                ...
            )
            :
            (
                ...
                {todos}
                ...
            )
        )
        :
        (
            <Redirect to="/" />
        )
    )
}
    

&lt;Post /&gt; 组件中的删除例程完成后使用redirect。这会将用户返回到路由器的 ToDoList 部分。显然,让用户看到刚刚被删除的路由器的空白部分看起来不太好!

所以,&lt;Header /&gt; 组件的 Switch 部分现在看起来像:

<Switch>
  <Route
    exact
    path="/"
    render={() => <Posts posts={this.state.posts} />}
  />
  <Route
    path="/post/:postSlug"
    render={(props) => {
      const post = this.state.posts.find(
        (post) => post.slug === props.match.params.postSlug
      );
      if (post)
        return (
                <ToDoList origin="post" posts={this.state.posts} id={post.id} />
        );
      else return <NotFound />;
    }}
  />
  <Route component={NotFound} />
</Switch>

而且,&lt;Posts /&gt; 组件看起来像:

class Posts extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    componentHandler.upgradeDom();
  }
  render() {
    return <ToDoList origin="posts" posts={this.props.posts} id="" />;
  }
}   

最后,&lt;Post /&gt; 组件看起来像:

class Post extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    componentHandler.upgradeDom();
  }
  render() {
    const display = this.props.id1 == this.props.id2 ? "block" : "none";
    const defaultStyle1 = {
      display: display
    };
    const defaultStyle2 = {
      padding: "20px"
    };
    return (
      <div className="demo-card-wide mdl-card mdl-shadow--2dp" style={defaultStyle1}>
        <div className="mdl-card__title post">
          <h2 className="mdl-card__title-text">{this.props.title}</h2>
        </div>
        <div className="mdl-card__supporting-text">
          {this.props.createdAt}
          <i className="fa fa-trash" onClick={this.props.onClickClose}></i>
        </div>
        <div className="mdl-card__actions mdl-card--border">
          <div className="todo-container" style={defaultStyle2}>
            {this.props.content}
          </div>
        </div>
      </div>
    );
  }
}

两个 id 属性从&lt;ToDoList /&gt; 组件发送到&lt;Post /&gt; 组件。这样可以在屏幕上显示正确的帖子。所有帖子实际上都被渲染了,但只有其中一个将其 CSS display 属性设置为 block。其余设置为none。现在,onClickClose 属性 [removeToDo 方法的别名] 可以传递给&lt;Post /&gt; 组件,它允许用户从路由器的帖子部分删除帖子.还可以使用相同的 removeToDo 方法从路由器的 ToDoList 部分删除每个帖子。

【讨论】:

    猜你喜欢
    • 2015-11-19
    • 2019-03-26
    • 2018-12-05
    • 1970-01-01
    • 2019-09-24
    • 2023-04-05
    • 1970-01-01
    • 2019-04-25
    • 1970-01-01
    相关资源
    最近更新 更多