0

It is my understanding that refs are not defined outside the react lifecycle (source). The problem I am trying to solve is to capture a key press at the document level (i.e. trigger the event no matter what element is in focus), and then interact with a react ref. Below is a simplified example of what I am trying to do:

export default class Console extends React.Component {
  
    constructor(props) {
        super(props);
        this.state = {
            visible: false,
            text: "",
        };
    }

    print(output: string) {
        this.setState({
            text: this.state.text + output + "\n"
        })
    }

    toggleVisible()
    {
        this.setState({visible: !this.state.visible});
    }

    render() {

        const footer_style = {
            display: this.state.visible ? "inline" : "none",
        };
        return (
            <footer id="console-footer" className="footer container-fluid fixed-bottom" style={footer_style}>
                <div className="row">
                    <textarea id="console" className="form-control" rows={5} readOnly={true}>{this.state.text}</textarea>
                </div>
            </footer>
        );
    }
}

class App extends React.Component {

  private console: Console;

  constructor() {
    super({});
    
    this.console = React.createRef();
  }

  keyDown = (e) =>
  {
      this.console.current.toggleVisible(); // <-- this is undefined
  }

  componentDidMount(){
      document.addEventListener("keydown", this.keyDown);
  },


  componentWillUnmount() {
      document.removeEventListener("keydown", this.keyDown);
  },

  render() {

    return (
      <div className="App" onKeyDown={this.keyDown}> // <-- this only works when this element is in focus
        // other that has access to this.console that will call console.print(...)
        <Console ref={this.console} />
      </div>
    );
  }
}

My question is: is there a way to have this sort of document level key press within the lifesycle of react so that the ref is not undefined inside the event handler keyDown? I've seen a lot of solutions that involve setting the tabIndex and hacking to make sure the proper element has focus at the right time, but these do not seem like robust solutions to me.

I'm just learning React so maybe this is a design limitation of React or I am not designing my components properly. But this sort of functionality seems quite basice to me, having the ability to pass components from one to the other and call methods on eachother.

McAngus
  • 1,826
  • 18
  • 34
  • I'm not sure what is not working for you, could you give steps to reproduce? https://codesandbox.io/s/icy-hooks-8zuy7?file=/src/App.js – walidvb Nov 06 '20 at 12:30

1 Answers1

1

You're calling the onKeyDown callback twice, once on document and once on App. Events bubble up the tree. When the textarea is not in focus, only document.onkeydown is called. When it is in focus, both document.onkeydown and App's div.onkeydown are called, effectively cancelling the effect(toggling state off and back on).

Here's a working example: https://codesandbox.io/s/icy-hooks-8zuy7?file=/src/App.js

import React from "react";

class Console extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
      text: ""
    };
  }

  print(output: string) {
    this.setState({
      text: this.state.text + output + "\n"
    });
  }

  toggleVisible() {
    this.setState({ visible: !this.state.visible });
  }

  render() {
    const footer_style = {
      display: this.state.visible ? "inline" : "none"
    };
    return (
      <footer
        id="console-footer"
        className="footer container-fluid fixed-bottom"
        style={footer_style}
      >
        <div className="row">
          <textarea id="console" className="form-control" rows={5} readOnly>
            {this.state.text}
          </textarea>
        </div>
      </footer>
    );
  }
}

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.console = React.createRef();
  }

  keyDown = (e) => {
    this.console.current.toggleVisible(); // <-- this is undefined
  };

  componentDidMount() {
    document.addEventListener("keydown", this.keyDown);
  }
  componentWillUnmount() {
    document.removeEventListener("keydown", this.keyDown);
  }

  render() {
    return (
      <div className="App" style={{ backgroundColor: "blueviolet" }}>
        enter key to toggle console
        <Console ref={this.console} />
      </div>
    );
  }
}

Also, I recommend using react's hooks:

export default App = () => {
  const console = React.createRef();

  const keyDown = (e) => {
    console.current.toggleVisible(); // <-- this is undefined
  };

  React.useEffect(() => {
    // bind onComponentDidMount
    document.addEventListener("keydown", keyDown);
    // unbind onComponentDidUnmount
    return () => document.removeEventListener("keydown", keyDown);
  });

  return (
    <div className="App" style={{ backgroundColor: "blueviolet" }}>
      press key to toggle console
      <Console ref={console} />
    </div>
  );
};
walidvb
  • 322
  • 1
  • 9
  • I was trying so many different ways to trigger the event, I left multiple in at once. Removing the onKeyDown from the `div` did the trick! – McAngus Nov 06 '20 at 13:05
  • great! If you want to know more about hooks, check out Amanda Wattenberger incredible post about them: https://wattenberger.com/blog/react-hooks ! – walidvb Nov 06 '20 at 19:53