Skip to content

Keeping it Simple (Refactoring)

Ok, so, you've worked for a while on your component, and you've come quite a way. The component does a number of different things. Great! But, the code is getting complicated and unwieldy…

It's time to refactor!

If you've never heard of refactoring, it's simply the process of changing the code without changing what the code does. This might sound counter-productive at first, but I promise you, it's the very opposite!

In this tutorial, I'll walk you through a real-world example of refactoring a component, going from a single large component, responsible for doing many different things, and in which almost everything is done by one giant render-method, to several small components, each responsible for a single part of the whole, and isolated so that changes to one part does not inadvertently affect another.

Below is a real-world example of a component in development. It's over 350 lines of code, and over 200 of those are in the render-method. Let's see what we can do to make this a little simpler to work with. (You don't need to read and understand the file below, it's just there for reference. We'll look at a few specific parts of it below.)

Please note: Since this code was a work in progress when copied to this tutorial, the functionality is incomplete, and there are several things that could be done to improve the code besides refactoring. In this tutorial, we will only be focusing on how to refactor the code.

import { Component, Element, h, Prop, State } from "@stencil/core";
// import { DateType } from '@limetech/lime-elements';
import { Option } from "@limetech/lime-elements";
import {
  LimeObject,
  LimeObjectRepository,
  LimeType,
  LimeWebComponent,
  LimeWebComponentContext,
  LimeWebComponentPlatform,
  SelectCurrentLimeObject,
  SelectCurrentLimeType,
  SelectCurrentUser,
} from "@limetech/lime-web-components";

const MIN_BVALUE = 0;
const MIN_REASON_LENGTH = 1;

@Component({
  tag: "lwc-solution-business-status",
  shadow: true,
  styleUrl: "lwc-solution-business-status.scss",
})
export class BusinessStatus implements LimeWebComponent {
  @Prop()
  public platform: LimeWebComponentPlatform;

  @Prop()
  public context: LimeWebComponentContext;

  @Element()
  public element: HTMLElement;

  @State()
  private datetime = new Date();

  @State()
  private isOpen = false;

  @State()
  private isOpen500k = false;

  @State()
  private isOpenTodo = false;

  @State()
  private isCreateTodo = false;

  @State()
  private wonlostreason = "";

  @State()
  private todoNote = "";

  @State()
  private bValue: number;

  @State()
  private isConfirmationOpen = false;

  @State()
  public basicValue: Option;

  @State()
  public businesstatusValue: Option;

  @State()
  public toggleValue: Option;

  @State()
  public disabled = false;

  @State()
  public optionGroup = 0;

  @SelectCurrentLimeObject()
  @State()
  private limeobject: LimeObject;

  @SelectCurrentLimeType()
  @State()
  private limetype: LimeType;

  private basicOptions: Option[] = [];

  componentWillLoad() {
    if (this.context.limetype === "company" && this.limeobject) {
      this.basicOptions = this.limetype["properties"]["businesstatus"][
        "options"
      ].map(function (i) {
        return { text: i.text, value: i.key };
      });
      this.businesstatusValue = this.basicOptions.find(
        (o) => o.value === this.limeobject["businesstatus"]
      );
    }
  }

  public render() {
    if (this.context.limetype === "company" && this.limeobject) {
      return [
        <limel-button
          primary={true}
          label="Change businesstatus"
          onClick={() => {
            this.isOpen = true;
          }}
        />,
        <limel-dialog
          heading="Change businesstatus"
          open={this.isOpen}
          closingActions={{ escapeKey: false, scrimClick: false }}
          onClose={() => {
            this.isOpen = false;
          }}
          onClosing={() => {
            this.isConfirmationOpen = true;
          }}
        >
          <form>
            <p>
              <limel-select
                options={this.basicOptions}
                value={this.businesstatusValue}
                label="Business Status"
                onChange={(event) => {
                  if (event.detail) {
                    this.businesstatusValue = event.detail as Option;
                  }
                }}
              />
              <p>Value: {JSON.stringify(this.businesstatusValue)}</p>
            </p>
            {this.businesstatusValue["value"] === "tender" &&
            this.limeobject["tendervalue"] === 0 ? (
              <p>
                <limel-input-field
                  label="Quotevalue"
                  key="tender"
                  value={
                    this.bValue ? this.bValue : this.limeobject["tendervalue"]
                  }
                  required={true}
                  formatNumber={true}
                  type="number"
                  invalid={!this.bValueValid()}
                  onChange={(event) => {
                    this.bValue = +event.detail;
                  }}
                />
              </p>
            ) : (
              <p />
            )}
            {this.businesstatusValue["value"] === "agreement" &&
            this.limeobject["businessvalue"] === 0 ? (
              <p>
                <limel-input-field
                  label="Businessvalue"
                  key="agreement"
                  value={
                    this.bValue ? this.bValue : this.limeobject["businessvalue"]
                  }
                  formatNumber={true}
                  type="number"
                  invalid={!this.bValueValid()}
                  onChange={(event) => {
                    this.bValue = +event.detail;
                  }}
                />
              </p>
            ) : (
              <p />
            )}
            {this.businesstatusValue["value"] === "agreement" ? (
              <p>
                <limel-input-field
                  label="Won reason"
                  required={true}
                  key="agreement"
                  value={this.wonlostreason}
                  invalid={!this.wonLostReasonValid()}
                  onChange={(event) => {
                    this.wonlostreason = event.detail;
                  }}
                />
              </p>
            ) : (
              <p />
            )}
            {this.businesstatusValue["value"] === "rejection" ? (
              <p>
                <limel-input-field
                  label="Lost reason"
                  required={true}
                  key="rejection"
                  value={this.wonlostreason}
                  invalid={!this.wonLostReasonValid()}
                  onChange={(event) => {
                    this.wonlostreason = event.detail;
                  }}
                />
              </p>
            ) : (
              <p />
            )}
          </form>
          <limel-button-group class="reverse-order" slot="button">
            <limel-button
              primary={true}
              label="Save"
              disabled={!this.bValueValid()}
              onClick={() => {
                this.submitForm();
                console.log(this.bValue);
                console.log(this.wonlostreason);
                this.isCreateTodo = true;
                if (this.bValue > 500000) {
                  this.isOpen500k = true;
                }
              }}
            />
            <limel-button label="Cancel" onClick={this.closeDialog} />
          </limel-button-group>
        </limel-dialog>,

        // TODO
        <limel-dialog
          open={this.isCreateTodo}
          closingActions={{ escapeKey: false, scrimClick: false }}
          onClose={() => {
            this.isCreateTodo = false;
          }}
        >
          <p>Do you wanna create a ToDo?</p>
          <limel-button-group class="reverse-order" slot="button">
            <limel-button
              primary={true}
              label="Yes"
              onClick={() => {
                this.isOpenTodo = true;
                this.isCreateTodo = false;
              }}
            />
            <limel-button
              label="No"
              onClick={() => {
                this.isCreateTodo = false;
              }}
            />
          </limel-button-group>
        </limel-dialog>,

        <limel-dialog
          heading="Todo"
          open={this.isOpenTodo}
          closingActions={{ escapeKey: false, scrimClick: false }}
          onClose={() => {
            this.isOpenTodo = false;
          }}
        >
          <form>
            <p>
              <limel-input-field
                label="Comment"
                value={this.todoNote}
                onChange={(event) => {
                  this.todoNote = event.detail;
                }}
              />
            </p>
          </form>
          <p>
                                
            <limel-date-picker
              type="datetime"
              label="Date"
              value={this.datetime}
              onChange={(event) => {
                // tslint:disable-line:jsx-no-lambda prettier
                this.handleChange(event);
              }}
            />
                                
            <p style={{ "font-size": "small" }}>
                                      Value:                         
              <code>
                                            
                {this.datetime
                  ? this.datetime.toString()
                  : JSON.stringify(this.datetime)}
                                        
              </code>
                                  
            </p>
                            
          </p>

          <limel-button-group class="reverse-order" slot="button">
            <limel-button
              primary={true}
              label="Save todo"
              onClick={this.createTodo}
            />
            <limel-button
              label="Cancel"
              onClick={() => {
                this.isOpenTodo = false;
              }}
            />
          </limel-button-group>
        </limel-dialog>,

        // Credit check
        <limel-dialog
          open={this.isOpen500k}
          closingActions={{ escapeKey: false, scrimClick: false }}
          onClose={() => {
            this.isOpen500k = false;
          }}
        >
          <p>THIS IS A LOT OF MONEY.</p>
          <limel-button-group slot="button">
            <limel-button
              label="Ok"
              onClick={() => {
                this.isOpen500k = false;
              }}
            />
          </limel-button-group>
        </limel-dialog>,
      ];
    }
  }

  private bValueValid() {
    return this.bValue > MIN_BVALUE;
  }

  private wonLostReasonValid() {
    return this.wonlostreason.length >= MIN_REASON_LENGTH;
  }

  private submitForm = () => {
    let body = {
      status: this.businesstatusValue["value"],
      businessvalue: this.bValue,
      wonlostreason: this.wonlostreason,
      business: this.context.id,
    };
    console.log(body);
    this.saveData(body);
    this.closeDialog();
  };

  private createTodo = () => {
    let body = {
      status: this.businesstatusValue["value"],
      note: this.todoNote,
      business: this.context.id,
    };
    console.log(body);
    // this.saveTodo(this.todoNote);
    console.log(this.todoNote);
  };

  private async saveData(body) {
    // const url = 'changebusinesstatus/update/';
    const objectService: LimeObjectRepository = this.platform.state
      .limeobjects;

    // Call endpoint that will save some data on the object
    await this.platform.http.post("changebusinesstatus/update/", body);

    // Manually refresh the object in the webclient, if the method is available
    if (objectService.reloadObject) {
      objectService.reloadObject(this.context.limetype, this.context.id);
    }
  }

  // private async saveTodo (body) {

  //     // const http: HttpService = this.platform.http;
  //     // const url = `my_addon/my_endpoint/?id=${this.context.id}`;
  //     const objectService: LimeObjectRepository = this.platform.state.limeobjects;

  //     // Call endpoint that will save some data on the object
  //     // await http.put(url, data);

  //     // Manually refresh the object in the webclient, if the method is available
  //     // if (objectService.reloadObject) {
  //     //     objectService.reloadObject(this.context.limetype, this.context.id);
  //     // }
  // }

  private closeDialog = () => {
    this.isOpen = false;
  };

  private handleChange(event) {
    this.datetime = event.detail;
  }
}

Breaking out functions

Let's focus on that render-method for now. Here it is again, but this time, I've removed a lot of detail that we don't need to care about right now:

    public render() {

        if (this.context.limetype === "company" && this.limeobject) {

            return [
                <limel-button
                    label="Change businesstatus"
                    onClick={() => {
                        this.isOpen = true;
                    }}
                />,


                <limel-dialog
                    heading="Change businesstatus"
                    open={this.isOpen}
                >
                    <form>
                    </form>
                    <limel-button-group class="reverse-order" slot="button">
                        <limel-button
                            label="Save"
                            onClick={() => {
                                this.isCreateTodo = true;
                                if (this.bValue > 500000) {
                                    this.isOpen500k = true;
                                }
                            }}
                        />
                        <limel-button label="Cancel" onClick={this.closeDialog} />
                    </limel-button-group>
                </limel-dialog>,


                // TODO
                <limel-dialog
                    open={this.isCreateTodo}
                >
                    <p>Do you wanna create a ToDo?</p>
                    <limel-button-group class="reverse-order" slot="button">
                        <limel-button
                            label="Yes"
                            onClick={() => {
                                this.isOpenTodo = true
                                this.isCreateTodo = false
                            }}
                        />
                        <limel-button label="No" onClick={() => {this.isCreateTodo = false}} />
                    </limel-button-group>
                </limel-dialog>,


                <limel-dialog
                    heading="Todo"
                    open={this.isOpenTodo}
                >
                    <form>
                    </form>
                    <limel-button-group class="reverse-order" slot="button">
                        <limel-button
                            label="Save todo"
                            onClick={this.createTodo}
                        />
                        <limel-button label="Cancel" onClick={ () => {this.isOpenTodo = false;}} />
                    </limel-button-group>
                </limel-dialog>,


                // Credit check
                <limel-dialog
                    open={this.isOpen500k}
                >
                    <p>THIS IS A LOT OF MONEY.</p>
                    <limel-button-group slot="button">
                        <limel-button
                            label="Ok"
                            onClick={() => {
                                this.isOpen500k = false;
                            }}
                        />
                    </limel-button-group>
                </limel-dialog>,
            ];
        }
    }

We can see that there is a button, followed by a series of dialogs. When the user clicks the button, the first dialog is opened. If the user clicks the save-button in that dialog, the second dialog is opened, and so on.

Let's start by putting each dialog in a private method, called by the render-method:

    private renderChangeStatusDialog() {
        return (
            <limel-dialog
                heading="Change businesstatus"
                open={this.isOpen}
                closingActions={{ escapeKey: false, scrimClick: false }}
                onClose={() => {
                    this.isOpen = false;
                }}
                onClosing={() => {
                    this.isConfirmationOpen = true;
                }}
            >
                <form>
                    <p>
                        <limel-select
                            options={this.basicOptions}
                            value={this.businesstatusValue}
                            label="Business Status"
                            onChange={event => {
                                if (event.detail) {
                                    this.businesstatusValue = event.detail as Option;
                                };
                            }}
                        />
                        <p>Value: {JSON.stringify(this.businesstatusValue)}</p>
                    </p>
                    {this.businesstatusValue['value'] === 'tender' && this.limeobject['tendervalue'] === 0

                    ?<p>
                        <limel-input-field
                            label="Quotevalue"
                            key="tender"
                            value={this.bValue ?this.bValue :this.limeobject['tendervalue']}
                            required={true}
                            formatNumber={true}
                            type="number"
                            invalid={!this.bValueValid()}
                            onChange={event => {
                                this.bValue = +event.detail;
                            }}
                        />
                    </p>
                    :<p />
                    }
                    {this.businesstatusValue['value'] === 'agreement' && this.limeobject['businessvalue'] === 0

                    ?<p>
                        <limel-input-field
                            label="Businessvalue"
                            key="agreement"
                            value={this.bValue ?this.bValue :this.limeobject['businessvalue']}
                            formatNumber={true}
                            type="number"
                            invalid={!this.bValueValid()}
                            onChange={event => {
                                this.bValue = +event.detail;
                            }}
                        />
                    </p>
                    :<p />
                    }
                    {this.businesstatusValue['value'] === 'agreement'
                    ?<p>
                        <limel-input-field
                            label="Won reason"
                            required={true}
                            key="agreement"
                            value={this.wonlostreason}
                            invalid={!this.wonLostReasonValid()}
                            onChange={event => {
                                this.wonlostreason = event.detail;
                            }}
                        />
                    </p>
                    :<p />
                    }
                    {this.businesstatusValue['value'] === 'rejection'
                    ?<p>
                        <limel-input-field
                            label="Lost reason"
                            required={true}
                            key="rejection"
                            value={this.wonlostreason}
                            invalid={!this.wonLostReasonValid()}
                            onChange={event => {
                                this.wonlostreason = event.detail;
                            }}
                        />
                    </p>
                    :<p />
                    }
                </form>
                <limel-button-group class="reverse-order" slot="button">
                    <limel-button
                        primary={true}
                        label="Save"
                        disabled={!this.bValueValid()}
                        onClick={() => {
                            this.submitForm();
                            console.log(this.bValue);
                            console.log(this.wonlostreason);
                            this.isCreateTodo = true;
                            if (this.bValue > 500000) {
                                this.isOpen500k = true;
                            }
                        }}
                    />
                    <limel-button label="Cancel" onClick={this.closeDialog} />
                </limel-button-group>
            </limel-dialog>
        );
    }

    private renderCreateTodoDialog() {
        return (
            <limel-dialog
                open={this.isCreateTodo}
                closingActions={{ escapeKey: false, scrimClick: false }}
                onClose={() => {
                    this.isCreateTodo = false;
                }}
            >
                <p>Do you wanna create a ToDo?</p>
                <limel-button-group class="reverse-order" slot="button">
                    <limel-button
                        primary={true}
                        label="Yes"
                        onClick={() => {
                            this.isOpenTodo = true
                            this.isCreateTodo = false
                        }}
                    />
                    <limel-button label="No" onClick={() => {this.isCreateTodo = false}} />
                </limel-button-group>
            </limel-dialog>
        );
    }

    private renderOpenTodoDialog() {
        return (
            <limel-dialog
                heading="Todo"
                open={this.isOpenTodo}
                closingActions={{ escapeKey: false, scrimClick: false }}
                onClose={() => {
                    this.isOpenTodo = false;
                }}
            >
                <form>
                    <p>
                        <limel-input-field
                            label="Comment"
                            value={this.todoNote}
                            onChange={event => {
                                this.todoNote = event.detail;
                            }}
                        />
                    </p>
                </form>
                <p>
                    <limel-date-picker
                        type="datetime"
                        label="Date"
                        value={this.datetime}
                        onChange={event => { // tslint:disable-line:jsx-no-lambda prettier
                            this.handleChange(event);
                        }}
                    />
                    <p style={{ 'font-size': 'small' }}>
                        Value:{' '}
                        <code>
                            {this.datetime
                                ? this.datetime.toString()
                                : JSON.stringify(this.datetime)}
                        </code>
                    </p>
                </p>

                <limel-button-group class="reverse-order" slot="button">
                    <limel-button
                        primary={true}
                        label="Save todo"
                        onClick={this.createTodo}
                    />
                    <limel-button label="Cancel" onClick={ () => {this.isOpenTodo = false;}} />
                </limel-button-group>
            </limel-dialog>
        );
    }

    private renderCreditCheckDialog() {
        return (
            <limel-dialog
                open={this.isOpen500k}
                closingActions={{ escapeKey: false, scrimClick: false }}
                onClose={() => {
                    this.isOpen500k = false;
                }}
            >
                <p>THIS IS A LOT OF MONEY.</p>
                <limel-button-group slot="button">
                    <limel-button
                        label="Ok"
                        onClick={() => {
                            this.isOpen500k = false;
                        }}
                    />
                </limel-button-group>
            </limel-dialog>
        );
    }

This is already a slight improvement. Since we gave each function a descriptive name, it's easier to understand what each dialog is for.

Let's look at what's left of the render-method:

    public render() {

        if (this.context.limetype === "company" && this.limeobject) {

            return [
                <limel-button
                    primary={true}
                    label="Change businesstatus"
                    onClick={() => {
                        this.isOpen = true;
                    }}
                />,

            ];
        }
    }

Not very much, as it turns out. But, right now, the dialogs are never going to be rendered at all, only the button is. Let's fix that:

    public render() {

        if (this.context.limetype === "company" && this.limeobject) {

            return [
                <limel-button
                    primary={true}
                    label="Change businesstatus"
                    onClick={() => {
                        this.isOpen = true;
                    }}
                />,
                this.renderChangeStatusDialog(),
                this.renderCreateTodoDialog(),
                this.renderOpenTodoDialog(),
                this.renderCreditCheckDialog(),
            ];
        }
    }

That's pretty nice, isn't it?

The renderChangeStatusDialog function is still pretty complicated, so let's break that down in similar fashion:

    private renderChangeStatusDialog() {
        return (
            <limel-dialog
                heading="Change businesstatus"
                open={this.isOpen}
                closingActions={{ escapeKey: false, scrimClick: false }}
                onClose={() => {
                    this.isOpen = false;
                }}
                onClosing={() => {
                    this.isConfirmationOpen = true;
                }}
            >
                <form>
                    {this.renderStatusSelect()}
                    {this.renderQuoteValueInput()}
                    {this.renderBusinessValueInput()}
                    {this.renderWonReasonInput()}
                    {this.renderLostReasonInput()}
                </form>
                {this.renderButtons()}
            </limel-dialog>
        );
    }

    private renderStatusSelect() {
        return (
            <p>
                <limel-select
                    options={this.basicOptions}
                    value={this.businesstatusValue}
                    label="Business Status"
                    onChange={event => {
                        if (event.detail) {
                            this.businesstatusValue = event.detail as Option;
                        };
                    }}
                />
                <p>Value: {JSON.stringify(this.businesstatusValue)}</p>
            </p>
        );
    }

    private renderQuoteValueInput() {
        if (this.businesstatusValue['value'] === 'tender' && this.limeobject['tendervalue'] === 0) {
            return (
                <p>
                    <limel-input-field
                        label="Quotevalue"
                        key="tender"
                        value={this.bValue ?this.bValue :this.limeobject['tendervalue']}
                        required={true}
                        formatNumber={true}
                        type="number"
                        invalid={!this.bValueValid()}
                        onChange={event => {
                            this.bValue = +event.detail;
                        }}
                    />
                </p>
            );
        }
    }

    private renderBusinessValueInput() {
        if (this.businesstatusValue['value'] === 'agreement' && this.limeobject['businessvalue'] === 0) {
            return (
                <p>
                    <limel-input-field
                        label="Businessvalue"
                        key="agreement"
                        value={this.bValue ?this.bValue :this.limeobject['businessvalue']}
                        formatNumber={true}
                        type="number"
                        invalid={!this.bValueValid()}
                        onChange={event => {
                            this.bValue = +event.detail;
                        }}
                    />
                </p>
            );
        }
    }

    private renderWonReasonInput() {
        if (this.businesstatusValue['value'] === 'agreement') {
            return (
                <p>
                    <limel-input-field
                        label="Won reason"
                        required={true}
                        key="agreement"
                        value={this.wonlostreason}
                        invalid={!this.wonLostReasonValid()}
                        onChange={event => {
                            this.wonlostreason = event.detail;
                        }}
                    />
                </p>
            );
        }
    }

    private renderLostReasonInput() {
        if (this.businesstatusValue['value'] === 'rejection') {
            return (
                <p>
                    <limel-input-field
                        label="Lost reason"
                        required={true}
                        key="rejection"
                        value={this.wonlostreason}
                        invalid={!this.wonLostReasonValid()}
                        onChange={event => {
                            this.wonlostreason = event.detail;
                        }}
                    />
                </p>
            );
        }
    }

    private renderButtons() {
        return (
            <limel-button-group class="reverse-order" slot="button">
                <limel-button
                    primary={true}
                    label="Save"
                    disabled={!this.bValueValid()}
                    onClick={() => {
                        this.submitForm();
                        console.log(this.bValue);
                        console.log(this.wonlostreason);
                        this.isCreateTodo = true;
                        if (this.bValue > 500000) {
                            this.isOpen500k = true;
                        }
                    }}
                />
                <limel-button label="Cancel" onClick={this.closeDialog} />
            </limel-button-group>
        );
    }

Notice how every time we break a function down, we're left with descriptive function calls, and all the details of the implementation is moved further down in the file? This is a good thing. It makes it easy to get an overview of what a function does, without having to wade through a flood of implementation details. And if we need those details, we can just scroll down to where the relevant function is implemented.

The next step would be to break these functions out into separate components to simplify the code even more.

To be continued…