Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Router with - Ant Design Sider: how to populate content section with components for relevant menu item

I'm trying to use AntD menu sider like a tab panel.

I want to put components inside the content so that the content panel renders the related component when a menu item is clicked.

How do I get this structure to take components as the content for each menu item?

import React from 'react';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { PasswordForgetForm } from '../Auth/Password/Forgot';


const { Title } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;


class Dashboard extends React.Component {
  state = {
    collapsed: false,
  };

  onCollapse = collapsed => {
    console.log(collapsed);
    this.setState({ collapsed });
  };

  render() {
    return (
      <Layout style={{ minHeight: '100vh' }}>
        <Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse}>
          <div  />
          <Menu theme="light" defaultSelectedKeys={['1']} mode="inline" >

            <Menu.Item key="1">
              <Icon type="fire" />
              <span>Next item</span>
              <PasswordForgetForm />
            </Menu.Item>  
            <Menu.Item key="2">
              <Icon type="fire" />
              <span>Next item</span>
              <Another component to render in the content div />
            </Menu.Item>

          </Menu>
        </Sider>
        <Layout>
          <Header style={{ background: '#fff', padding: 0 }} />
          <Content style={{ margin: '0 16px', background: '#fff' }}>

            <div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
            RENDER RELEVANT COMPONENT HERE BASED ON MENU ITEM SELECTED
            </div>
          </Content>
          </Layout>
      </Layout>
    );
  }
}

export default Dashboard;

When I try to render this, no errors are thrown, but the content div does not update with the PasswordForgetForm that I specified as the content for the menu item.

I tried Chris' suggestion below - which works fine to render the component for each of the different menu item content divs in the layout segment, however - the downside of this approach is that with every page refresh, the path goes to the content component for that particular menu item, instead of the test page which has the menu on it -- and the content component with that relevant content. If this approach is endorsed as sensible, is there a way to keep the page refresh on the original page, rather than trying to go to a subset menu item url?

Update

I found this literature. I think this might be what I need to know, but the language is too technical for me to understand. Can anyone help with a plain english version of this subject matter?

I also found this tutorial which uses Pose to help with rendering. While this structure is what I'm trying to achieve, it looks like Pose has been deprecated. Does anyone know if its necessary to go beyond react-router to get this outcome, or is there a solution within react that I can look to implement?

This example is similar to what I want, but I can't find anything that defines sidebar (which seems to be an argument that is necessary to make this work).

I have also seen this post. While some of the answers are a copy and paste of the docs, I wonder if the answer by Adam Gering is closer to what I need. I'm trying to keep the Sider menu on the /dash route at all times. That url should not change - regardless of which menu item in the sider the user clicks BUT the content div in the /dash component should be updated to render the component at the route path I've specified.

APPLYING CHRIS' SUGGESTION

Chris has kindly offered a suggestion below. I tried it, but the circumstance in which it does not perform as desired is when the menu item is clicked (and the relevant component correctly loads in the content div, but if I refresh the page, the refresh tries to load a page with a url that is for the component that is supposed to be inside the content div on the dashboard page.

import React from 'react';
import {
    BrowserRouter as Router,
    Route,
    Link,
    Switch,

  } from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';

// import UserName from '../Users/UserName';
import Account from '../Account/Index';
import Test from '../Test/Index';



const { Title } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;


class Dashboard extends React.Component {
  state = {
    collapsed: false,
  };

  onCollapse = collapsed => {
    console.log(collapsed);
    this.setState({ collapsed });
  };

  render() {
    const {  loading } = this.state;
    // const dbUser = this.props.firebase.app.snapshot.data();
    // const user = Firebase.auth().currentUser;
    return (
    <AuthUserContext.Consumer>
      {authUser => (  

        <div>    
         {authUser.email}
          <Router>  
            <Layout style={{ minHeight: '100vh' }}>
              <Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse}>
                <div  />
                <Menu theme="light" defaultSelectedKeys={['1']} mode="inline" >
                  <SubMenu
                    key="sub1"
                    title={
                      <span>
                      <Icon type="user" />
                      <span>Profile</span>
                      </span>
                    }
                  >

                      <Menu.Item key="2"><Link to={ROUTES.ACCOUNT}>Account Settings</Link></Menu.Item>


                      <Menu.Item key="3"><Link to={ROUTES.TEST}>2nd content component</Link></Menu.Item>
</SubMenu>

                </Menu>
              </Sider>
              <Layout>

                <Header>                </Header>      
                <Content style={{ margin: '0 16px', background: '#fff' }}>

                  <div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
                      <Switch>
                          <Route path={ROUTES.ACCOUNT}>
                          <Account />
                          </Route>
                          <Route path={ROUTES.TEST}>
                          < Test />
                          </Route>

                      </Switch>    
                  </div>
                </Content>
                <Footer style={{ textAlign: 'center' }}>


                 test footer
                </Footer>
              </Layout>
            </Layout>
          </Router>  
        </div>
      )}
    </AuthUserContext.Consumer>  
    );
  }
}

export default Dashboard;

In my routes file, I have:

export const ACCOUNT = '/account';

I've also found this tutorial - which uses the same approach outlined above - and doesn't get redirected in the same way that my code does on page refresh. The tutorial uses Route instead of BrowserRouter - but otherwise I can't see any differences.

NEXT ATTEMPT

I saw this post (thanks Matt). I have tried to follow the suggestions in that post.

I removed the outer Router wrapper from the Dashboard page (there is a Router around App.js, which is where the route to Dashboard is setup).

Then, in my Dashboard, I changed the Content div to:

I added:

let match = useRouteMatch();

to the render method inside my Dashboard component (as shown here).

I added useRouteMatch to my import statement from react-router-dom.

This produces an error that says:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks

There is more message in the error above but stack overflow won't allow me to post it because it has a short link

I don't know what I've done wrong in these steps, but if I comment out the let statement, the page loads.

When I go to /Dashboard and click "account", Im expecting the account component to render inside the content div I have in the layout on the Dashboard page. Instead, it redirects directly to a page at localhost3000/account (for which there is not page reference - it's just a component to render inside the Dashboard page).

So this is worse than the problem I started with - because at least at the start, the redirect only happened on page refresh. Now it happens immediately.

I found this repo that has examples of each kind of route. I can't see what it is doing that I am not.

This post seems to have had the same problem as me, and resolved it using the same approach as I have tried. It's a post from 2018 so may the passage of time makes the approach outdated - but I can't see any difference between what that post solution implements and my original attempt.

NEXT ATTEMPT

I had thought I had found an approach that works in Lyubomir's answer on this post.

I have:

<Menu.Item key="2">
                      <Link to=

{${this.props.match.url}/account}> Account Settings

Then in the div on the dash component where I want to display this component I have:

<div>
                      <Switch>
                          <Route exact path={`${this.props.match.url}/:account`} component={Account} />

This works. On page refresh, I can keep things working as they should.

HOWEVER, when I add a second menu item with:

<Menu.Item key="16">
                    <Link to={`${this.props.match.url}/learn`}>
                      <Icon type="solution" />
                      <span>Lern</span>
                    </Link>  
                  </Menu.Item>

Instead of rendering the Learning component when menu item for /learn is clicked (and the url bar changes to learn), the Account component is rendered. If i delete the account display from the switch statement, then I can have the correct Learning component displayed.

With this attempt, I have the menu items working to match a Dashboard url extension (ie localhost/dash/account or localhost/dash/learn) for ONE menu item only. If I add a second menu item, the only way I can correctly render the 2nd component, is by deleting the first menu item. I am using switch with exact paths.

I want this solution to work with multiple menu items.

I have tried alternating path for url (eg:

<Link to={`${this.props.match.url}/learn`}>

is the same as:

<Link to={`${this.props.match.path}/learn`}>

I have read the explanation in this blog and while I don't entirely understand these options. I have read this. It suggests the match statement is now a legacy method for rendering components (now that hooks are available). I can't find any training materials that show how to use hooks to achieve the expected outcome.

like image 451
Mel Avatar asked Jan 10 '20 02:01

Mel


People also ask

What is the BrowserRouter /> component?

BrowserRouter: BrowserRouter is a router implementation that uses the HTML5 history API(pushState, replaceState and the popstate event) to keep your UI in sync with the URL. It is the parent component that is used to store all of the other components.


3 Answers

The better solution is using React Router <Link> to make each menu item link to a specific path, and then in the content, using <Switch> to render the corresponding component. Here's the doc: React router

Render With React Router

<Router>
  <Layout style={{ minHeight: "100vh" }}>
    <Sider
      collapsible
      collapsed={this.state.collapsed}
      onCollapse={this.onCollapse}
    >
      <div />
      <Menu theme="light" defaultSelectedKeys={["1"]} mode="inline">
        <Menu.Item key="1">
          // Add a react router link here
          <Link to="/password-forget-form">
            <Icon type="fire" />
            <span>Next item</span>
          </Link>
        </Menu.Item>
        <Menu.Item key="2">
          // Add another react router link
          <Link to="/next-item">
            <Icon type="fire" />
            <span>Next item</span>
          </Link>
        </Menu.Item>
      </Menu>
    </Sider>
    <Layout>
      <Header style={{ background: "#fff", padding: 0 }} />
      <Content style={{ margin: "0 16px", background: "#fff" }}>
        <div style={{ padding: 24, background: "#fff", minHeight: 360 }}>
          // Render different components based on the path
          <Switch>
            <Route path="/password-forget-form">
              <PasswordForgetForm />
            </Route>
            <Route path="/next-item">
              <Another component to render in the content div />
            </Route>
          </Switch>
        </div>
      </Content>
    </Layout>
  </Layout>
</Router>;

Render With Menu Keys

App.js

import React, { useState } from "react";
import { Layout } from "antd";
import Sider from "./Sider";
import "./styles.css";

const { Content } = Layout;
export default function App() {
  const style = {
    fontSize: "30px",
    height: "100%",
    display: "flex",
    alignItems: "center",
    justifyContent: "center"
  };

  const components = {
    1: <div style={style}>Option 1</div>,
    2: <div style={style}>Option 2</div>,
    3: <div style={style}>Option 3</div>,
    4: <div style={style}>Option 4</div>
  };

  const [render, updateRender] = useState(1);

  const handleMenuClick = menu => {
    updateRender(menu.key);
  };

  return (
    <div className="App">
      <Layout style={{ minHeight: "100vh" }}>
        <Sider handleClick={handleMenuClick} />
        <Layout>
          <Content>{components[render]}</Content>
        </Layout>
      </Layout>
    </div>
  );
}

Sider.js

import React from "react";
import { Menu, Layout, Icon } from "antd";

const { SubMenu } = Menu;

export default function Sider(props) {
  const { handleClick } = props;
  return (
    <Layout.Sider>
      <Menu theme="dark" mode="inline" openKeys={"sub1"}>
        <SubMenu
          key="sub1"
          title={
            <span>
              <Icon type="mail" />
              <span>Navigation One</span>
            </span>
          }
        >
          <Menu.Item key="1" onClick={handleClick}>
            Option 1
          </Menu.Item>
          <Menu.Item key="2" onClick={handleClick}>
            Option 2
          </Menu.Item>
          <Menu.Item key="3" onClick={handleClick}>
            Option 3
          </Menu.Item>
          <Menu.Item key="4" onClick={handleClick}>
            Option 4
          </Menu.Item>
        </SubMenu>
      </Menu>
    </Layout.Sider>
  );
}

Edit antd-menu-click-rendering

like image 192
Wei Su Avatar answered Oct 09 '22 13:10

Wei Su


I found Lyubomir's answer on this post. It works. Nested routes with react router v4 / v5

The menu item link is:

<Menu.Item key="2">
                      <Link to=
                       {`${this.props.match.url}/account`}>
                        Account
                      </Link>
                    </Menu.Item>

The display path is:

<Route exact path={`${this.props.match.path}/:account`} component={Account} />

There is a colon before the name of the component. Not sure why. If anyone knows what this approach is called - I'd be grateful for a reference so that i can try to understand it.

like image 3
Mel Avatar answered Oct 09 '22 14:10

Mel


I am using antd >=4.20.0, as a side effect Menu.Item is deprecated. While above solution works, It does warn about discontinuing support. So thats not a long term solution.

Check here for details on 4.20.0 changes.

Long term solution is that instead of Menu.Item we need to use items={[]}. But with that <Link> doesn't work as API doesn't have support and we cant pass it as child component.

I looked around, after much search and combining information from various sources, what worked was,

  1. use useNavigate() to change pages using onClick in Menu items[].
  2. use useLocation().pathname to get path and pass it up as selectedKey
  3. Define routes in <Content>.

Here's working example [ stripped to skeleton code to illustrate concept]

#index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';

<React.StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
  </React.StrictMode> 

#App.js

import React, {useState} from 'react';
import { Layout, Menu, Tooltip } from 'antd'
import {
  MenuUnfoldOutlined,
  MenuFoldOutlined,
  TeamOutlined,
  UserOutlined,
} from '@ant-design/icons'
import { useLocation, useNavigate, Route, Routes } from 'react-router-dom'

import './App.less';
import 'antd/dist/antd.min.css';

const { Content, Sider } = Layout;

const Page1 = () => {
 return <h4> Page 1</h4>
}

const Page2 = () => {
  return <h4> Page 2</h4>
}


const App = () => {
  const [collapsed, setCollapsed] = useState(true);

  const toggleCollapsed = () => {
    setCollapsed(!collapsed);
  };

  let navigate = useNavigate();
  const selectedKey = useLocation().pathname

  const highlight = () => {
    if (selectedKey === '/'){
      return ['1']
    } else if (selectedKey === '/page2'){
      return ['2']
    }  
  }

 

  return (
    <Layout className="site-layout">
      <Sider trigger={null} collapsible collapsed={collapsed}>
        <div className="logo"> 
            <Tooltip placement="right" arrowPointAtCenter  title="Expand / Shrink Menu" >
                {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
                    className: 'trigger',
                    onClick: toggleCollapsed,
                })}
            </Tooltip>
          </div>
          <Menu
            mode="inline"
            theme="light"
            defaultSelectedKeys={['1']}
            selectedKeys={highlight()}
            style={{ height: '100%', borderRight:0 }}
            items={[
              {
                key: '1',
                icon: <UserOutlined />,
                label: "Page 1",
                onClick: () => { navigate('/')}
              },
              {
                key: '2',
                icon: <TeamOutlined />,
                label: "Page 2",
                onClick: () => { navigate('/page2')}
              }

            ]}
            />
        </Sider>
        <Content>
          <Routes>
            <Route exact path="/" element={<Page1 />} />
            <Route path="/page2" element={<Page2 />} />
          </Routes>
        </Content>
    </Layout>
  )
}

export default App;

#Resulting Page enter image description here

like image 3
Anil_M Avatar answered Oct 09 '22 13:10

Anil_M