7
\$\begingroup\$

I wrote a React app to adjust the serving size of a recipe. For example, if the recipe serves 4 people and requires 4 eggs, scaling it down to serve only 1 person should only require 1 egg. I try to also keep the units singular/plural accordingly while converting units when they become too big or too small. These features are written in plain JS, but I'd be curious to hear if it would be more appropriate to do this in React and how.

Relevant code snippets:

const RECIPES = [{
  id: 'pasta-dough',
  name: 'Pasta Dough',
  serves: 4,
  ingredients: [
    {quantity: '4', unit: 'eggs'},
    {quantity: '4', unit: 'cups', item: 'flour'},
    // Note that the quantity below would normally be '1/4'.
    {quantity: '.25', unit: 'cup', item: 'olive oil'},
    {quantity: '1', unit: 'tablespoon', item: 'salt'}
  ]
}];

const Recipe = React.createClass({
  getInitialState: function() {
    return {
      serves: this.props.serves,
      scale: 1
    };
  },
  onChange: function(e) {
    const serves = Math.floor(Math.abs(e.target.value));
    this.setState({
      serves: serves,
      scale: serves / this.props.serves
    });
  },
  render: function() {
    return (
      <section>
        <h1>{this.props.name}</h1>
        <h2>
          Serves
          <input
            type="number" min="0"
            value={this.state.serves}
            /* TODO: Smarter default step. */
            step={this.props.serves / 4}
            onChange={this.onChange} />
        </h2>
        <Ingredients ingredients={this.props.ingredients} scale={this.state.scale} />
      </section>
    );
  }
});

const Ingredients = React.createClass({
  render: function() {
    return (
      <ul>
        {this.props.ingredients.map((ingredient, i) =>
          <Ingredient key={i} scale={this.props.scale} {...ingredient} />
        )}
      </ul>
    );
  }
});

const Ingredient = React.createClass({
  render: function() {
    // Commenting out the code that depends on other code libraries.
    // Want to keep this code review as simple as possible.
    // const quantity = new Quantity(this.props.quantity).multiply(this.props.scale);
    const unit = this.props.unit;
    // const measurement = new Measurement(quantity, unit).convertUnits();
    const quantity = this.props.quantity * this.props.scale;
    const measurement = `${quantity} ${unit}`
    return(
      <li>{measurement.toString()} {this.props.item}</li>
    );
  }
});

RECIPES.forEach((recipe, i) => {
  ReactDOM.render(
    <Recipe key={recipe.id} index={i} {...recipe}/>,
    document.getElementById(recipe.id));
});

document.querySelector('h2 input').focus();
<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="pasta-dough"></div>

Demo: https://rviscomi.github.io/scale-recipe-react/
Source: https://github.com/rviscomi/scale-recipe-react

I'm new to React and would really appreciate advice on writing more semantic React. Some specific areas I'm interested in:

  • use of state vs props
  • mounting
  • component organization
  • static methods

Things I'm not interested to get feedback on:

  • general JS style
  • ES6, unless it directly affects React
  • markup semantics
\$\endgroup\$
2
  • \$\begingroup\$ "I try to also keep the units singular/plural accordingly while converting units" What happened to that? Pluralization works on your other demo, but not with the Stack Snippet. \$\endgroup\$
    – Laurel
    Commented Aug 17, 2016 at 1:57
  • \$\begingroup\$ @Laurel There are some comments in the Ingredient renderer to explain what happened to that. Basically, I'm omitting those dependencies to focus the review on the React code. \$\endgroup\$ Commented Aug 17, 2016 at 5:30

1 Answer 1

1
\$\begingroup\$

I have edited your little code snippet and wrote it in ES6, cause sooner or later you will need it. Most tutorials are written in ES6. Mostly you will use a build tool like webpack in building react apps.

Example structure:

  • dist/
    • js/
      • bundle.js
    • css/
      • style.css
    • index.html
  • src/

    • libs/ (js libs)
    • components/ (one folder for every component to have all files together e.g. css with the use of css modules)
      • Ingredient/
        • Ingredient.jsx
        • Ingredient.css
      • Ingredients/
        • Ingredients.jsx
        • Ingredients.css
    • App.jsx
    • style.css
  • webpack.config.js

Some useful links:

const RECIPES = [{
    id: 'pasta-dough',
    name: 'Pasta Dough',
    serves: 4,
    ingredients: [
        {quantity: '4', unit: 'eggs'},
        {quantity: '4', unit: 'cups', item: 'flour'},
        // Note that the quantity below would normally be '1/4'.
        {quantity: '.25', unit: 'cup', item: 'olive oil'},
        {quantity: '1', unit: 'tablespoon', item: 'salt'}
    ]
}];


class Ingredient extends React.Component {
    render() {
        let quantity = this.props.quantity * this.props.scale;

        return (
            <li>
              {quantity} {this.props.unit} {this.props.item}
            </li>
        );
    }
}


class Ingredients extends React.Component {
    render() {
        return (
            <ul>
                {this.props.ingredients.map((ingredient, i) =>
                    <Ingredient key={i} scale={this.props.scale} {...ingredient} />
                )}
            </ul>
        );
    }
}


class Recipe extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            serves: this.props.serves,
            scale: 1
        }

        this.onChange = this.onChange.bind(this);
    }

    onChange(e) {
        const serves = Math.floor(Math.abs(e.target.value));

        this.setState({
            serves: serves,
            scale: serves / this.props.serves
        });
    }


    render() {
        return (
            <section>
                <h1>{this.props.name}</h1>
                <h2>
                    Serves
                    <input
                        type="number" min="0"
                        value={this.state.serves}
                        /* TODO: Smarter default step. */
                        step={this.props.serves % 4}
                        onChange={this.onChange}/>
                </h2>
                <Ingredients ingredients={this.props.ingredients} scale={this.state.scale}/>
            </section>
        );
    }
}


class App extends React.Component {
    render() {
        return <div>
            {RECIPES.map((recipe, i) => {
                return <Recipe {...recipe} key={i}/>;
            })}
        </div>;
    }
}


ReactDOM.render(
    <App />,
    document.getElementById('recipes'));
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <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>

</head>
<body>
<div id="recipes"></div>
</body>
</html>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.