Text Recorder: React States, Event Handling and Conditional Rendering

Text Recorder: React States, Event Handling and Conditional Rendering

·

15 min read

React stopped being scary by now and that's very good news. Now, we can turn any HTML code into React components, isn't that something? We've made it this far and now it's time to take a step further and get to play around with something awesome!

States

We owe Javascript a lot of things, but the most magical of them all is page interactivity. We can click, hover and do all sorts of things and the page will instantly respond and rearrange itself magically based on our actions.

Of course, React is not built to deny us these magical features, on the contrary, it is built to make our lives even easier when using them and that's where states come in handy.

We can keep our data saved in states and whenever a state is changed, it is designed to automatically reflect its change smoothly and effortlessly in all the locations it is used.

States are similar to Props. They help us play around with our data, but the difference is states allow us to change the original value while props can't be changed. They are immutable. So, if we know for a fact we are going to change a certain value, then we should declare it as a state not a prop.

States are available in Class Components. We can access our states easily in a similar manner to props using this.state

And we can set it using 2 different ways:

  • Directly in the constructor using an object with all our states, but this method can ONLY be used in the constructor and nowhere else

      constructor(){
          super();
          this.state={
              iAmAState:"",
              iAmAnotherState:[]
          };
      }
    
  • Using setState() and that can be used wherever we want to update a state. We pass it an object with only the affected state and it merges the changes for us

      this.setState({
          iAmAState:"I am updated!"
      });
    

Event Handling

We have been talking about state changes and for those changes to take place we are going to need something to trigger them. That's where events enter.

We love events and we always listen to them. *wink*

Setting up our components to handle events is not very different from normal HTML elements, as an example to handle a click event on a button we set the onclick attribute to call a Javascript function <button onclick="myJavascriptFunction()">Click me</button>

In JSX the naming is a little different, it's usually camelCase of the original, so onclick turns into onClick and is assigned using a curly brace that is passed the name of the method within the class using this <button onClick={this.handleClick}>Click me</button>

I have also noticed that handle + the name of the event is used as a naming convention for the method executing the handling code. Not sure why, but I'm going to follow the herd until further notice.

Now that we can handle our events, let's work that brain and build something!

Build What?

I am new to technical blogging. My first post in this series was my first post ever. In the writing process, I started facing some challenges, one of which was code snippets. Sometimes I want to emphasize something by showing the sequence the code was written in. A GIF would do for illustrations, but not for code snippets as it would deny us the ultimate developer skill of copying and pasting and I would not allow that on my watch! Also, I have a word for you: Accessibility. So that leaves us with including multiple divided snippets of the same block. As I wrote, I sat there daydreaming if only there was a text recorder! And there it was my next coding task!

I am going to be my own knight with a shining React armor and with the help of states, I should start creating the text recorder of my dreams and hopefully, later, I'll continue working on it and make it embed-able somehow to save myself the drama... or not. The future remains a mystery.

You are allowed to play around for a little while HERE first

text recorder gif I am a smart text recorder

What's The Plan?

What we want is to press record, write our text, stop recording and then play what we have recorded with the ability to reset and start over with another text.

To do that, all we need is a text area and a bunch buttons, so I think we can fit it all in one TextRecorder component.

Sorry to inform you, that our plan is to approach this the stupid way. We are going to track every key press and save the whole text in an object along with the current timestamp. From that history we will easily be able to get the sequence the text was written in and the time difference between each key stroke and the previous one.

Sounds like a great stupid plan, let's start coding!

TextRecorder

As usual we're going to start by running npx create-react-app . in our root folder of choice, then we are going to create the folder structure we agreed upon previously while creating our mini calendar to create a TextRecorder component.

We are going to create a components folder in the src folder, add to it a TextRecorder folder and inside it we are going to create a TextRecorder.js file and a TextRecorder.css file.

Previously, while creating our mini calendar, we used function components, now since we are using states, it's time to use class components.

import React from "react";
import "./TextRecorder.css";

class TextRecorder extends React.Component {
  render() {
    return <div className="text-recorder"></div>;
  }
}

export default TextRecorder;

Now, let's clear the App.css file and import our TextRecorder in App.js to view our lonely component on screen as we work.

import React from "react";
import "./App.css";
import TextRecorder from "./components/TextRecorder/TextRecorder";

function App() {
  return (
    <div className="app">
      <TextRecorder />
    </div>
  );
}

export default App;

Great! Now back to our TextRecorder, what do we need?

  • A header
  • A text area
  • some buttons
<div className="text-recorder">
  <h1 className="header">Text Recorder</h1>
  <textarea
    className="text"
    rows="10"
    placeholder="Press Record and start typing..."
  ></textarea>
  <div className="controls">
    <button className="btn">Record</button>
    <button className="btn">Stop Recording</button>
    <button className="btn">Play</button>
    <button className="btn">Reset</button>
  </div>
</div>

To make things a little better for the eyes, let's add our CSS into our TextRecorder.css

.text-recorder {
  display: flex;
  flex-direction: column;
  justify-items: center;
  width: 80vw;
}

.text-recorder .header {
  text-align: center;
}

.text-recorder .text {
  background-color: #16202c;
  box-shadow: white 0px 0px 5px 3px;
  border: none;
  color: white;
  padding: 15px;
  border-radius: 10px;
  font-size: 16px;
  width: 80%;
  margin: 0 auto;
  margin-bottom: 20px;
}

.controls {
  display: flex;
  justify-content: space-evenly;
}

.controls .btn {
  padding: 10px;
  background-color: #16202c;
  border: none;
  border-radius: 100px;
  box-shadow: white 0px 0px 4px 0px;
  color: white;
  cursor: pointer;
}

And App.css

* {
  box-sizing: border-box;
}

html,
body,
#root {
  height: 100%;
}

#root {
  width: 100%;
  display: flex;
  justify-content: center;
  background-color: #16202c;
  color: white;
}

The buttons look okay, but to me they seem like they need icons to spice things up a little.

I am going to use Font Awesome icons so to add them to our project let's run

npm install @fortawesome/fontawesome-svg-core
npm install @fortawesome/free-solid-svg-icons
npm install @fortawesome/react-fontawesome

After that, let's import our icons of choice in our component. I chose those gems:

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faRecordVinyl,
  faStop,
  faPlay,
  faRedo,
} from "@fortawesome/free-solid-svg-icons";

Now, let's add them to our buttons. I added a custom color to brighten things up.

<div className="controls">
  <button className="btn">
    <FontAwesomeIcon icon={faRecordVinyl} color="#c51818" /> Record
  </button>
  <button className="btn">
    <FontAwesomeIcon icon={faStop} color="#c51818" /> Stop Recording
  </button>
  <button className="btn">
    <FontAwesomeIcon icon={faPlay} color="#c51818" /> Play
  </button>
  <button className="btn">
    <FontAwesomeIcon icon={faRedo} color="#c51818" /> Reset
  </button>
</div>

Looking good! Let's get our buttons working!

We are going to start with our Record button. We will need to track our click event and handle it using handleRecord method in our class

handleRecord=()=>{

}

We agreed that camelCase is a winner, so onClick it is in our button.

<button className="btn" onClick={this.handleRecord}>
  <FontAwesomeIcon icon={faRecordVinyl} color="#c51818" /> Record
</button>

Once I press record, I would want to record my first timestamp in my history with the current value of the text area so let's set our first states!

We will load our constructor first thing in our class and make sure we call super() to trigger our super class constructor, as well. Our component is not passed any props, but if it were, we would pass a props variable to them both. After that, we will set three states, one to store all our history, the other to keep track of the text written in our text area and one to indicate that the record button was pressed.

constructor(){
    super();
    this.state={
        history:{},
                text:"",
                record:false
    };
}

Remember, we are only allowed to set state directly in the constructor, but whenever we need to change our state later like in handleRecord, we will always use this.setState({})

handleRecord = () => {
  let history = {};
  history[new Date().getTime()] = this.state.text;
  this.setState({
    history,
    record:true
  });
};

Usually, if we call this.setState in a method, we will need to bind this method first or else we will get a type error that considers our variable undefined. To avoid that, we can either bind it in the constructor using this.handleRecord = this.handleRecord.bind(this) OR we can use arrow functions and it will automatically handle the binding for us.

Nice! Now after we know that recording has started we need to go to our next step and it is saving the history of every key up event with its timestamp.

Similar to handleRecord we will create a handleKeyUp method and call it in our text area

<textarea
  className="text"
  rows="10"
  placeholder="Press Record and start typing..."
  onKeyUp={this.handelKeyUp}
></textarea>

We will need to check first if our record button was pressed using our record flag state, then add to our history the text area value.

handleKeyUp = () => {
  if (this.state.record) {
    let history = this.state.history;
    history[new Date().getTime()] = this.state.text;
    this.setState({
      history
    });
  }
};

Theoretically, that's great but so far, we haven't set the text area value to reflect in the text state. Let's get on with it, then!

We want to turn it into a controlled component. We will add a handleTextChange method and update the text state in it whenever the text area is changed to make React control its value, hence, the name.

handleTextChange = (e) => {
  this.setState({
    text: e.target.value,
  });
};

Then we will add it to our onChange event in the text area

<textarea
  className="text"
  rows="10"
  placeholder="Press Record and start typing..."
  onKeyUp={this.handelKeyUp}
  onChange={this.handleTextChange}
></textarea>

Now everything is synced together let's stop that recording!

Of course, we will have a handleStopRecording method, in which all we need to do, is just set our record flag to false.

handleStopRecording = () => {
  this.setState({
    record: false
  });
};

Let's not forget to add it to the onClick attribute in our button

<button className="btn" onClick={this.handleStopRecording}>
  <FontAwesomeIcon icon={faStop} color="#c51818" /> Stop Recording
</button>

Here comes our most important feature *drumroll*

The Play button!

We will add handlePlay as expected and set it in the play button.

<button className="btn" onClick={this.handlePlay}>
  <FontAwesomeIcon icon={faPlay} color="#c51818" /> Play
</button>

In our handlePlay method, we will start by clearing the current text state, then we will get all our stored timestamps which are the history object keys. We will loop after that, through all the timestamps getting the time difference between each entry and the previous one and wait this amount of time before setting the text state with our value, which of course, will be automatically reflected in our text area.

handlePlay = () => {
    this.setState({
      text: ""
  });
    let keys = Object.keys(this.state.history);
    let prevTimestamp = keys.shift();
    let time = 0;
    keys.forEach((timestamp) => {
      time += (timestamp - prevTimestamp);
      setTimeout(() => {
        this.setState({ text: this.state.history[timestamp] });
      }, time);
      prevTimestamp=timestamp;
    });
};

Hooray! We just played our recorded text!

Now, the hard part is over we need one more method to handle our reset button.

Guess what we're going to name it?

<button className="btn" onClick={this.handleReset}>
  <FontAwesomeIcon icon={faRedo} color="#c51818" /> Reset
</button>

In handleReset we're just going to return all our variables to their original state like in the constructor to be able to start over with a new recording.

handleReset = () => {
  this.setState({
    history: {},
    text: "",
    record: false
  });
};

Alright, all our functionality is complete!

I don't like having all the buttons laid next to each other at the same time, though. Let's play around to make them only visible when needed! Here is what we are aiming at:

  • At the beginning only the Record button should be visible
  • After we hit Record only the Stop Recording should be visible
  • After the Stop Recording button is hit, both the Play and Reset buttons should be visible
  • When the Reset button is hit we will go back to only the Record button and so on

Conditional Rendering

We have three ways for conditional rendering

  • We can return entirely different blocks of JSX depending on the condition
  • We can store the elements to be rendered in a variable according to the condition and call it later using curly braces in our return statement
  • We can use ternary operations within the JSX code since they are Javascript expressions as long as they are enclosed in curly braces

For my buttons, I believe the second way is the most suitable way. We would be storing the buttons to be rendered in a variable and call it later in our controls block.

At first, we are going to declare a controls variable and set it with the record button as a default value since that is expected to be our initial state.

let controls = (
  <button className="btn" onClick={this.handleRecord}>
    <FontAwesomeIcon icon={faRecordVinyl} color="#c51818" /> Record
  </button>
);

Then, I am going to comment out all my buttons for now and call the controls variable until we make sure everything is working as expected

<div className="controls">
  {controls}
  {/* <button className="btn" onClick={this.handleRecord}>
    <FontAwesomeIcon icon={faRecordVinyl} color="#c51818" /> Record
  </button>
  <button className="btn" onClick={this.handleStopRecording}>
    <FontAwesomeIcon icon={faStop} color="#c51818" /> Stop Recording
  </button>
  <button className="btn" onClick={this.handlePlay}>
    <FontAwesomeIcon icon={faPlay} color="#c51818" /> Play
  </button>
  <button className="btn" onClick={this.handleReset}>
    <FontAwesomeIcon icon={faRedo} color="#c51818" /> Reset
  </button> */}
</div>

After that, we're going to check our record flag to see if recording was triggered and in that case make only the Stop Recording button visible.

let controls = (
  <button className="btn" onClick={this.handleRecord}>
    <FontAwesomeIcon icon={faRecordVinyl} color="#c51818" /> Record
  </button>
);
if (this.state.record) {
  controls = (
    <button className="btn" onClick={this.handleStopRecording}>
      <FontAwesomeIcon icon={faStop} color="#c51818" /> Stop Recording
    </button>
  );
}

To detect that the Stop Recording button was pressed, we're going to need to add one more flag similar to the record flag, let's call it stop

We will add it in the constructor

constructor() {
  super();
  this.state = {
    history: {},
    text: "",
    record: false,
    stop: false,
  };
}

Update it in handleStopRecording

handleStopRecording = () => {
  this.setState({
    record: false,
    stop: true,
  });
};

And clear it in handleReset

handleReset = () => {
  this.setState({
    history: {},
    text: "",
    record: false,
    stop: false
  });
};

After that, we need to add another condition to render the play and reset button in case that flag was set to true. We're going to wrap our buttons in a <React.Fragment> element since we want to return multiple elements

if (this.state.stop) {
  controls = (
    <React.Fragment>
      <button className="btn" onClick={this.handlePlay}>
        <FontAwesomeIcon icon={faPlay} color="#c51818" /> Play
      </button>
      <button className="btn" onClick={this.handleReset}>
        <FontAwesomeIcon icon={faRedo} color="#c51818" /> Reset
      </button>
    </React.Fragment>
  );
}

And YAAAAAAAAY! We're done! We can clear all our commented buttons now!

The code can be found HERE

By this mini text recorder I shall end my third baby step towards React greatness, until we meet in another one.

Any feedback or advice is always welcome. Reach out to me here, on Twitter, there and everywhere!