Semantic-ui-react: <Tab /> re-renders child pane on switching.

Created on 19 Jul 2017  路  7Comments  路  Source: Semantic-Org/Semantic-UI-React

Steps

The default behavior of the <Tab /> component seems to be to remove it's active <Tab.Pane> from the DOM every time it is made inactive, and to push it back in the DOM when it is selected and made active. This re-renders the child components of <Tab.Pane> and is a problem if you have children like Leaflet or CesiumJS which internally manage their display state, as this state is reset on every re-render.

Expected Result

One should expect that the default behavior would be to keep all Tab.Pane children rendered and in the DOM, and only display the one that is active. The inactive tabs should be hidden from the display while keeping them in the DOM.

Actual Result

Inactive Tabs get removed from the DOM and pushed in again when made active. This behavior breaks the flow of many stateful child components, by resetting their internal state.

Version

v0.71.0

Any quick fixes would be appreciated, while this gets fixed in main.

enhancement

Most helpful comment

It seems that either behaviour (renderActiveOnly vs renderAll) could be justified

I would be OK supporting a single prop renderActiveOnly and default it to true so we have backward compatibility. Users can then renderActiveOnly={false} to render every tab.

Right now, we hard code the active className in TabPane.js. This will now need to become a prop. The Tab.js will then be able to choose to render all TabPanes and toggle the active prop or render a single Tab only (as it does currently).

All 7 comments

This is expected behaviour, see this comment. I will left this open until @levithomason will response.

@layershifter It seems that either behaviour (renderActiveOnly vs renderAll) could be justified based on the use case. If the use case involves heavy context switch penalty between tabs, renderActiveOnly behaviour would make Semantic UI unusable, and require careful unmounting/remounting support for Tab children.

BlueprintJS implements a 'renderActiveTabPanelOnly' prop to support both behaviours. It would be nice to see something like this in Semantic UI's <Tab /> component, unless there is another way to use Semantic UI to mimic Tab behaviour, but keep all tabs rendered.

I managed to get my desired behavior working by using a combination of <Menu /> and <Segment />.
I set the CSS visibility property of the Segments to toggle based on the active Menu Item. Still, a little roundabout way to achieve this behavior, but it works. Looking forward to having a prop enabling this OTB.

Pasting code here for the benefit of anyone facing a similar issue with using <Tab />.

JSX

import React, { Component } from 'react';
import { Menu, Segment } from 'semantic-ui-react';
import classNames from 'classnames';

import Map from '../containers/Map/Map';
import Dashboard from '../containers/Dashboard/Dashboard';
import Team from '../containers/Team/Team';
import styles from './MainTabs.css';

class MainTabs extends Component {

  constructor(props) {
    super(props);

    this.state = {
      activePage: 'Map'
    }

  }

  handleMenuClick = (e, { name }) => {
    this.setState({ activePage: name });
  }
  render() {
    const { activePage } = this.state;

    let MapClass = classNames({
      'hidden': (activePage !== 'Map')
    });

    let DashboardClass = classNames({
      'hidden': (activePage !== 'Dashboard')
    });

    let TeamClass = classNames({
      'hidden': (activePage !== 'Team')
    });

    return (
      <div>
        <Menu>
          <Menu.Item
            name="Map"
            active={activePage === 'Map'}
            onClick={this.handleMenuClick}
          />
          <Menu.Item
            name="Dashboard"
            active={activePage === 'Dashboard'}
            onClick={this.handleMenuClick}
          />
          <Menu.Item
            name="Team"
            active={activePage === 'Team'}
            onClick={this.handleMenuClick}
          />

        </Menu>


        <Segment className={MapClass}>
          <Map />
        </Segment>

    {/* ................. Similarly for other tabs................... */}
      </div>
    );
  }

}

export default MainTabs;

CSS

.hidden {
  visibility: hidden;
}

It seems that either behaviour (renderActiveOnly vs renderAll) could be justified

I would be OK supporting a single prop renderActiveOnly and default it to true so we have backward compatibility. Users can then renderActiveOnly={false} to render every tab.

Right now, we hard code the active className in TabPane.js. This will now need to become a prop. The Tab.js will then be able to choose to render all TabPanes and toggle the active prop or render a single Tab only (as it does currently).

A more adaptable quick-fix for the problem at hand starting from the implementation above. For this to work every child element put inside should have a name prop (which is used for the menu label).

import React, { Component } from "react";
import { Menu, Segment } from "semantic-ui-react";
import PropTypes from "prop-types";

class MainTabs extends Component {
  state = {
    activePage: ""
  };

  componentWillMount() {
    let { children } = this.props;
    this.setState({
      activePage: children[0].props.name
    });
  }

  handleMenuClick = ({ name }) => {
    this.setState({ activePage: name });
  };
  render() {
    let { activePage } = this.state;
    let { children } = this.props;

    return (
      <div>
        <Menu>
          {children.map((child, index) =>
            <Menu.Item
              key={index}
              name={child.props.name}
              active={activePage === child.props.name}
              onClick={() => this.handleMenuClick(child.props)}
            />
          )}
        </Menu>
        {children.map((child, index) =>
          <Segment
            key={index}
            style={{
              display: activePage !== child.props.name ? "none" : "block"
            }}
          >
            {child}
          </Segment>
        )}
      </div>
    );
  }
}

MainTabs.propTypes = {
  children: PropTypes.arrayOf(PropTypes.element).isRequired
};

export default MainTabs;

Usage example

<MainTab>
    <TabOne name="Tab one label name" key={0} />
    <TabTwo name="Tab two label name" key={1} />
</MainTab>

@chinoy Did #1976 resolve your issue with Leaflet, as in your example? Leaflet is why I searched for this issue as well, yet Leaflet appears to be not rendering correctly (zoomed all the way out) when rendered on an inactive tab. It works fine when the Leaflet tab is the first active tab. I.e.

Works (Leaflet is up first):

        const panes = [
            {menuItem: "Map", pane: {key: 'system-map', content: <MyMapPane/>}},
            {menuItem: "Description", pane: {key: 'description', content: "Some content" }}
        ]

Doesn't work (Leaflet is initially hidden):

        const panes = [
            {menuItem: "Description", pane: {key: 'description', content: "Some content" }},
            {menuItem: "Map", pane: {key: 'system-map', content: <MyMapPane/>}}
        ]

Any solution for this?. I have tried renderActiveOnly={false} , but not working for me.

Was this page helpful?
0 / 5 - 0 ratings