Watching state from child component React with Material UI

Viewed 2330

New to React. Just using create-react-app and Material UI, nothing else. Coming from an Angular background.

I cannot communicate from a sibling component to open the sidebar. I'm separating each part into their own files. I can get the open button in the Header to talk to the parent App, but cannot get the parent App to communicate with the child LeftSidebar.

Header Component

import React, {Component} from 'react';
import AppBar from 'material-ui/AppBar';
import IconButton from 'material-ui/IconButton';
import NavigationMenu from 'material-ui/svg-icons/navigation/menu';

class Header extends Component {

  openLeftBar = () => {
    // calls parent method
    this.props.onOpenLeftBar();
  }

  render() {
    return (
      <AppBar iconElementLeft={
                <IconButton onClick={this.openLeftBar}>
                  <NavigationMenu />
                </IconButton>
              }
      />
    );
  }
}

export default Header;

App Component -- receives event from Header, but unsure how to pass dynamic 'watcher' down to LeftSidebar Component

import React, { Component } from 'react';
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import RaisedButton from 'material-ui/RaisedButton';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';

// components
import Header from './Header/Header';
import Body from './Body/Body';
import Footer from './Footer/Footer';
import LeftSidebar from './LeftSidebar/LeftSidebar';

class App extends Component {
  constructor() {
    super() // gives component context of this instead of parent this
    this.state = {
      leftBarOpen : false
    }
  }

  notifyOpen = () => {
    console.log('opened') // works
    this.setState({leftBarOpen: true});
    /*** need to pass down to child component and $watch somehow... ***/
  }

  render() {
    return (
      <MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
        <div className="App">

          <Header onOpenLeftBar={this.notifyOpen} />

          <Body />

          <LeftSidebar listenForOpen={this.state.leftBarOpen} />

          <Footer />

        </div>
      </MuiThemeProvider>
    );
  }
}

export default App;

LeftSidebar Component - cannot get it to listen to parent App component - Angular would use $scope.$watch or $onChanges

// LeftSidebar
import React, { Component } from 'react';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';
import IconButton from 'material-ui/IconButton';
import NavigationClose from 'material-ui/svg-icons/navigation/close';

class LeftNavBar extends Component {
  /** unsure if necessary here **/
  constructor(props, state) {
    super(props, state)

    this.state = {
      leftBarOpen : this.props.leftBarOpen
    }
  }

  /** closing functionality works **/
  close = () => {
    this.setState({leftBarOpen: false});
  }

  render() {
    return (
      <Drawer open={this.state.leftBarOpen}>
        <IconButton onClick={this.close}>
          <NavigationClose />
        </IconButton>
        <MenuItem>Menu Item</MenuItem>
        <MenuItem>Menu Item 2</MenuItem>
      </Drawer>
    );
  }
}

export default LeftSidebar;
2 Answers

Free your mind of concepts like "watchers". In React there is only state and props. When a component's state changes via this.setState(..) it will update all of its children in render.

Your code is suffering from a typical anti-pattern of duplicating state. If both the header and the sibling components want to access or update the same piece of state, then they belong in a common ancestor (App, in your case) and no where else.

(some stuff removed / renamed for brevity)

class App extends Component {
  // don't need `constructor` can just apply initial state here
  state = { leftBarOpen: false }
  // probably want 'toggle', but for demo purposes, have two methods
  open = () => { 
    this.setState({ leftBarOpen: true }) 
  }
  close = () => { 
    this.setState({ leftBarOpen: false }) 
  }
  render() {
    return (
      <div className="App">
        <Header onOpenLeftBar={this.open} />
        <LeftSidebar 
          closeLeftBar={this.close}
          leftBarOpen={this.state.leftBarOpen} 
        />
      </div>
    )
  }
}

Now Header and LeftSidebar do not need to be classes at all, and simply react to props, and call prop functions.

const LeftSideBar = props => (
  <Drawer open={props.leftBarOpen}>
    <IconButton onClick={props.closeLeftBar}>
      <NavigationClose />
    </IconButton>  
  </Drawer>
)

Now anytime the state in App changes, no matter who initiated the change, your LeftSideBar will react appropriately since it only knows the most recent props

Once you set the leftBarOpen prop as internal state of LeftNavBar you can't modify it externally anymore as you only read the prop in the constructor which only run once when the component initialize it self.
You can use the componentWillReceiveProps life cycle method and update the state respectively when a new prop is received.

That being said, i don't think a Drawer should be responsible for being closed or opened, but should be responsible on how it looks or what it does when its closed or opened.

A drawer can't close or open it self, same as a light-Ball can't turn it self on or off but a switch / button can and should.

Here is a small example to illustrate my point:

const LightBall = ({ on }) => {
  return (
    <div>{`The light is ${on ? 'On' : 'Off'}`}</div>
  );
}

const MySwitch = ({ onClick, on }) => {
  return (
    <button onClick={onClick}>{`Turn the light ${!on ? 'On' : 'Off'}`}</button>
  )
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      lightOn: false
    };
  }

  toggleLight = () => this.setState({ lightOn: !this.state.lightOn });

  render() {
    const { lightOn } = this.state;
    return (
      <div>
        <MySwitch onClick={this.toggleLight} on={lightOn} />
        <LightBall on={lightOn} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Related