0

I have a plain HTML page (part of a non-React application) with some text representing simple mathematical calculations. There are <span> elements containing the formulae, and next to them <span> elements with the calculations inside. Each span is represented with a generic class, either formula or calculation and also a specific class, such as formula-1 and calculation-1. Note that the number in the classes will always be the same for associated formulae and calculations. These calculations are also dynamic in number; there could be 1 on a page or 30.

I'm using a React component to replace the calculation spans with button elements, and when a button is clicked it re-renders to show the calculation value. Some sample HTML and the JS are show below.

This solution works, but my issue is that it feels dirty. I'm quite new to React and JavaScript isn't my primary language, but it seems to me like each calculation element should be represented by an instance of the component, rather than a single component looping through all of the calculations on a page and rendering each.

So my question is: is the below working solution incorrect in terms of how React should be used, and if so, what's the generally accepted correct manner of doing this?

<span class="formula formula-1" data-calculation="4">2 + 2</span> = <span class="calculation calculation-1>4</span>
<span class="formula formula-2" data-calculation="6">2 + 4</span> = <span class="calculation calculation-2>6</span>
<span class="formula formula-3" data-calculation="22">2 + 20</span> = <span class="calculation calculation-3>22</span>

I'm created a React component that replaces the

import React, {Component} from "react";
import ReactDOM from "react-dom";

class Calculate extends Component {

  constructor() {
    super();
    this.viewSolution = this.viewSolution.bind(this);
    this.state = {
      calculations: []
    };
    let formula_elements = document.getElementsByClassName('formula');
    for (var i = 0; i < formula_elements.length; i++) {
      this.state.calculations[i] = formula_elements[i].getAttribute('data-calculation');
    }
  }

  viewSolution(event) {
    this.setState({
      clicked: true
    });
  }

  render() {
    if (this.state.clicked === true) {
      return (
        <span>{this.state.calculations[this.props.index]}</span>
      );
    }
    return (
      <button className="btn" onClick={this.viewSolution}>View solution</button>
    );
  }

}

export default Calculate;

const calculation_elements = document.getElementsByClassName('calculation');

for (var i = 0; i < calculation_elements.length; i++) {
  ReactDOM.render(<Calculate index={i}/>, calculation_elements[i]);
}
Jamie Hollern
  • 154
  • 3
  • 15

1 Answers1

1

I offer one solution below. I'm not sure if you were wanting to view the solution of an equation entered by the user, because that can be done pretty easily with the use of input and the onChange function. Anyway, what is below is based off what I understood the problem to be.

Some notes:

  • The use of 'var' has taken a backseat to 'let' and 'const'. Read more about that at What's the difference between using "let" and "var" to declare a variable in JavaScript?
  • Since I am not saving props data into state, I didn't have a need for the actual constructor function.
  • I am not binding my functions since I am using arrow functions which do not have their own 'this'
  • I created a dictionary of the formulas and answers which I think would allow you to scale more easily.
  • Depending on your file structure, you may need to add an export for the App and Calculate component.

import React, { Component } from 'react';

class Calculate extends Component {
  state = {
    clicked: false
  };

  viewSolution = event => this.setState({ clicked: true });

  render() {
    const { clicked } = this.state;
    const { answer, calculation } = this.props;

    const element = clicked
      ? <span> = { answer }</span>
      : <button
          className='btn'
          onClick={ this.viewSolution }>
          View solution
        </button>;

    return <div>
      <span>{ calculation }</span>
      { element }
    </div>;
  }

}

class App extends Component {
  render() {
    const formulae = {
      ['2 + 2']: 4,
      ['2 + 4']: 6,
      ['2 + 20']: 22
    };

    return <div>
      {
        Object.keys(formulae).map((calculation, key) => (
          <Calculate
            answer={formulae[calculation]}
            calculation={calculation}
            key={key} />
        ))
      }
    </div>
  }
}

ReactDOM.render(<App />);
Ross Sheppard
  • 850
  • 1
  • 6
  • 14
  • Many thanks for your answer, it looks really good. Unfortunately, the component is being embedded on another non-React application and the formulas and answers have to be hard-coded in HTML on the page (it's a PHP script that generates the answers from the formulas and renders them). That's the reason the answers are stored as data attributes in the formula `` elements, so that the JS can read them and use the values. I know this is a bit of a silly way of doing this, but I can't get round these constraints on this particular occasion. – Jamie Hollern Oct 05 '18 at 16:31
  • 1
    Ah, I see now. You have static HTML with a React component being rendered somewhere else on screen (the button/answer). If it was purely a React application, I would say that you could also look into refs, but I don't think that will help you here either. Instead, I'd say that your solution is not dirty given the constraints, but the way the application being built may need to be revised. :( – Ross Sheppard Oct 05 '18 at 17:48