0

I want to be able to receive mqtt messages and display them on a web app. I'm working with AWS Amplify's PubSub and the function calls happen outside of the class component. I can't directly access any of the instance functions/properties from the function outside of it but I need some way to trigger a setState change so that the web app can be re-rendered, right?

I've tried just directly calling the function from the React class, but there's still no re-rendering happening. I've tried making an outside variable, storing the message received in it and then accessing the variable from the React class but still no trigger. Then I researched and found that I could force a re-render every couple of seconds but read that doing such a thing should be avoided. I should be using ComponentDidMount and setState functions but not really understanding how to get this to work.

Again all I'm really trying to do is get the message and update the web app to display the new message. Sounds pretty simple but I'm stuck.

import...

var itemsArr = [];

function subscribe() {
    // SETUP STUFF
    Amplify.configure({
        ...
    });
    Amplify.addPluggable(new AWSIoTProvider({
        ...
    }));

    // ACTUAL SUBSCRIBE FUNCTION
    Amplify.PubSub.subscribe('item/list').subscribe({
        next: data => {
        // LOG DATA
        console.log('Message received', data);

        // GET NAMES OF ITEMS
        var lineItems = data.value.payload.lineItems;
        lineItems.forEach(item => itemsArr.push(item.name));
        console.log('Items Ordered', itemsArr);

        // THIS FUNCTION CALL TRIGGERS AN ERROR
        // CANT ACCESS THIS INSTANCE FUNCTION
        this.update(itemsArr);
    },
        error: error => console.error(error),
        close: () => console.log('Done'),
    });
}

// REACT COMPONENT
class App extends Component {
    constructor(props){
        super(props);

        this.state = {
            items:null,
        };
    }

    // THOUGHT I COULD USE THIS TO UPDATE STATE
    // TO TRIGGER A RE-RENDER
    update(stuff){
        this.setState({items: stuff});
    }

    render() {
        // THINK SUBSCRIBE METHOD CALL SHOULDN'T BE HERE
        // ON RE-RENDER, KEEPS SUBSCRIBING AND GETTING
        // SAME MESSAGE REPEATEDLY
        subscribe();
        console.log('items down here', itemsArr);

        return (
            <div className="App">
                <h1>Test</h1>
                <p>Check the console..</p>
                <p>{itemsArr}</p>
            </div>
        );
    }
}
export default App;

Ideally, I'd like columns of the list of items to be displayed as messages come in but I'm currently getting an error - "TypeError: Cannot read property 'update' of undefined" because the subscribe function outside of the class doesn't have access to the update function inside the class.

Play
  • 3
  • 3
  • For starters, you're trying to call an instance method outside of the object instance. You can only call your update() inside the App class. – Amir Jun 28 '19 at 14:45

2 Answers2

0

Put subscribe method inside the App component so you can call it. You can call subscribe method in componentDidMount lifecycle to execute it (to get the items) after App component renders the first time. And then, update method will run this.setState() (to store your items in the state) causing App component to re-render. Because of this re-render, your this.state.items will contain something and it will be displayed in your paragraph.

class App extends Component {
  constructor(props){
    super(props);

    this.state = {
        items: [],
    };

    this.subscribe = this.subscribe.bind(this); 
    this.update = this.update.bind(this);
  }

  update(stuff){
    this.setState({ items: stuff });
  }

  subscribe() {
    // SETUP STUFF
    Amplify.configure({
      ...
    });
    Amplify.addPluggable(new AWSIoTProvider({
      ...
    }));

    // ACTUAL SUBSCRIBE FUNCTION
    Amplify.PubSub.subscribe('item/list').subscribe({
      next: data => {
      // LOG DATA
      console.log('Message received', data);

      // GET NAMES OF ITEMS
      let itemsArr = [];
      var lineItems = data.value.payload.lineItems;
      lineItems.forEach(item => itemsArr.push(item.name));
      console.log('Items Ordered' + [...itemsArr]);
      this.update(itemsArr);
    },
      error: error => console.error(error),
      close: () => console.log('Done'),
    });
  }

  componentDidMount() {
    this.subscribe();
  }

  render() {

    console.log('items down here ' + [...this.state.items]);

    return (
        <div className="App">
            <h1>Test</h1>
            <p>Check the console..</p>
            <p>{this.state.items !== undefined ? [...this.state.items] : "Still empty"}</p>
        </div>
    );
  }
}

export default App;
0

By using an Object as a prop which stores references to internal methods of a child-component, it is possible to then access them from the parent.

The below (overly-simplified) example shows this. The RandomNumber purpose is to generate a single random number. It's all it does.

Then, at its parent-level, some user action (button onClick event) is triggering the RandomNumber component to re-render, using a custom hook called useShouldRender, which generates a random number every time it's setter function is invoked, so by exposing the setter function to the "exposed" prop object, it is possible to interact with internal component operations (such as re-render)

const {useState, useMemo, useReducer} = React

// Prints a random number
const RandomNumber = ({exposed}) => {
  // for re-render (https://stackoverflow.com/a/66436476/104380)
  exposed.reRender = useReducer(x => x+1, 0)[1]; 
  return Math.random(); // Over-simplification. Assume complex logic here.
}

// Parent component
const App = () => {
  // create a memoed object, which will host all desired exposed methods from
  // a child-component to the parent-component:
  const RandomNumberMethods = useMemo(() => ({}), [])

  return (
      <button onClick={() => RandomNumberMethods.reRender()}>
        <RandomNumber exposed={RandomNumberMethods}/>
      </button>
  )
}

// Render 
ReactDOM.render(<App />, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
vsync
  • 118,978
  • 58
  • 307
  • 400