Full Application

« Return to the Chapter Index

Table of contents
  1. Full Application
  2. Movie Records
    1. Gathering Requirements
    2. Sketching the Application
    3. Developing the Data Model
      1. Rendering the Movie List
      2. Rendering the Ratings
      3. Rendering the Song List
      4. Viewing the App
      5. Messing with State
      6. Watching a Movie
      7. Editing a Movie
      8. Editing a Song
      9. Deleting a Movie
      10. Adding a Movie
    4. Testing the Application
    5. Iterate
  3. đź“ť Task - Quizzer
    1. Create the Branch
    2. Task Requirements
    3. First Steps
      1. Existing Code
    4. Grading
      1. Sketch
      2. Completing Requirements
    5. Submission
    6. Creative Freedom

Building a real application is difficult. You may understand each piece, but that is not the same as knowing how to glue all the pieces together. Most of the work you do as a software engineer is actually gluing existing components together, in fact. Granted, here we are going to have to make the new components too, and that will be hard too.

The real trick, however, is that all of this becomes easier with good planning. You never just jump into coding without thinking, you really want to follow a process. A simplified version of a more sophisticated process we will eventually show you is as follows:

  1. Gather requirements
  2. Sketch the application
  3. Develop the data model
  4. Build a “Minimally Viable” version
  5. Iterate

Movie Records

A client has requested a website where they can record movies that they are interested in watching or have already watched, in order to share with their friends when deciding movie night. Eventually, the website will need a sophisticated backend to persist data long term, but for now they want a simple site that works with some data they have already prepared. Dubbing this application the Movie Records site, they have contracted us to make a Minimal Viable version.

You can see the final result of what we produced on this site: https://frontend-fun.github.io/movie-records/

Screenshot of the final version of the Movie Records website

Gathering Requirements

The first order of business was to meet with the client and discuss what exactly they wanted the application to do. This led to a list of user requirements.

  • Users must be able to view a list of movies
  • Users can see the title, description, release date, and rating of the movie
  • Users should be able to edit movie’s attributes
  • Users should be able to add a new movie
  • Users should be able to remove an existing movie
  • Users can indicate if they have seen and/or liked a movie
  • Users should be able to quickly play the trailer of the movie
  • Users should be able to play some of the movie’s soundtrack

We would probably want more detail in this list, and we should also get a better handle on who the users will be and what their specific needs will be. But for now, we will stick to this simple list.

Sketching the Application

With the requirements in hand, we sat down and made an initial sketch of the Movie Records application’s View. This could have been done with software like Figma or PowerPoint, but we used pencil-and-paper (whiteboards are also popular). In some places, we added annotations to highlight interactive components or aspects of the Control that might not be visible.

An initial sketch of the Movie Records application

This initial version incorporated features that we did not end up implementing, like the ability to filter records or to insert movies at arbitrary places. We also did not end up following this exact UI placement for some elements (e.g., the buttons, the editor layout).

However, the value of the sketch is in syncing up the development team and ironing out some important questions in a more concrete way. Another benefit is that this is relatively easy to translate to specific components once we got started on development.

We showed this sketch to the client (making sure to remind them that it was a very rough draft) and made sure they were happy with what they say. They mentioned how some of the features were less important (e.g., filtering, keywords), and that helped convince us to drop them from our minimal viable product.

Developing the Data Model

Far more important than any actual code is the development of the data model for our application. Remember, the State lives at the heart of all applications - all the control flow exists merely in service of manipulating the state. You should spend time before making components thinking about the state of your application, and where that state lives.

“Where the State lives” in React means, what components need to call useState, so that the relevant state variables and state setters can be passed down to the children who need them. Your goal is to keep the state high enough up in the hierarchy that everyone has access who needs it, without going higher than necessary. Unfortunately, that often means that the highest component (which might be App, but might be some other specialized component like MovieRecords or MovieApp that lives in App) will contain a signficant amount of State. That State might only be in a single data structure (an Array, Object, or Record), but will still represent most of the data for the application.

Here were our initial thoughts on the State:

  • At the top-level, there is an array of movies
  • Each movie has a title, release year, description, rating, and a unique ID
  • Each movie has an array of songs
  • A song has a name, who made the song, and a unique ID
  • Each movie can either be seen or unseen, liked or not liked, and we wanted to track when that information changes

These thoughts about the state led us to the following three Interfaces:

As part of this process, we also made up some mock data to play with. This ended up being useful for testing our application too.

For the unique IDs in our movies, we actually used the ID for the movie’s trailer on YouTube. Similarly, for our songs’ unique IDs, we used their Spotify ID. This had a huge advantage when it came time to embed the trailers and songs into our webpage. There was a draw-back since we then couldn’t edit the trailer video of a Movie in our application (since that uniquely identified the Movie!). Since we were making our own data, we had the freedom to make this choice, but often you will find that data comes pre-equipped with a good unique ID.

Notice that we do not actually store the Watch information in our Movie records in the example data. This is because we felt that that state was special and different from our movie records. First, that data is not intrinsic to the movie, so it felt inappropriate to keep it directly next to the description and released date. Further, we predict future versions of this application where multiple users will have their own watch records, and the movie records will eventually need to be kept more distinct from that watch data. Planning for the future is a tricky game - its entirely possible we will never end up making a future version, or we’ll need to solve a completely different problem, or we’ll want a different solution. Still, trying to plan a few steps ahead can pay off. Never let yourself get mired in pre-optimization, though, and remember YAGNI.

After our application imports data initially, we need to make sure that the final result is a valid Movie object according to our interface before we use the data as our initial state. This means we must also include the watched attribute with the appropriate object inside. To that end, we had a little bit of logic in our application to handle pre-processing the movie data. Note that we carefully placed this logic OUTSIDE of the component; otherwise it will be unnecessarily recalculated each time our application renders. We only need to run this code once to set up our initial data.

If you look at that last link, when we actually use the initial data post-processing, the line is shockingly simple. We use the MOVIES constant as the argument to useState. Yet the resulting movies state variable represents the vast majority of the data for our application! This can be a little disorienting for folks getting started, but just keep in mind how much data that little state variable is pointing to: multiple levels of nested arrays and objects!

Rendering the Movie List

Since we have the data for our Movie List, we want to be able to see the Movies. This will let us know if our data is sane, and give us a better idea of what the application should look like. In the App component, we instantiated the MovieList component.

The MovieList component is really just responsible for calling map on the movies in order to in turn build up a whole stack of MovieView components. Notice how much we have to pass around state and setters, even though this component isn’t using the state and setters themselves very much!

The MovieView component has a lot going on inside of it, hinting at all the future work that it will do. But it actually started off quite small. Looking carefully through the earlier version, you’ll see that we basically were just trying to render all the fields and not worry about anything else.

In both the earlier version and the final version, one of the most important things we do is to show the title, release, and description fields.

However, you can see that there are also some more complicated components, like the Ratings and the SongList. Let’s take a closer look at how we developed each one.

Rendering the Ratings

We wanted a fun visual to show off the ratings of movies, instead of just a raw number. The ratings originally came from IMDB, which operated on a 10-point scale. We converted the scale to a 5-star rating using a little math (divide by two, round up). Then, we create two strings of either filled stars or empty stars based on the calculated rating, using the built-in repeat method of strings. The final component may look flashy, but the component is less complex than some of the later ones.

Rendering the Song List

On the other hand, the logic needed for the SongList is quite complicated. In fact, the logic mirrors what we needed to do for the MovieList component. The SongList component is largely just responsible for using map to render the list of songs, delegating most of the actual work for rendering each song to a child component PlaySong.

This time, instead of a Stack component from Bootstrap, we use the ListGroup component (you can learn more about ListGroup from React Bootstrap’s documentation). The design of the user interface is complex, and not something we can really explain here. But you should know that we had a lot of options, and there are many right answers for how to lay out the user interface of the application!

Our final version of the PlaySong component relies on an HTML iframe to embed Spotify content into our application. Although a little complicated, the end result is fully playable versions of our songs. Recall that we used the Spotify ID as the unique ID for each song, so we can just interpolate (insert) the song.id into the appropriate URL that Spotify provides developers. We use the same trick with the MovieTrailer component.

At an earlier point, we were actually just rendering out the text of the song’s name and composer. You can see that simpler component below, which takes advantage of some Bootstrap classes to make the layout a little prettier. You can learn more about the classes we used in Bootstrap’s Spacing and Text docs.

export function PlaySong({ song }: { song: Song }): JSX.Element {
  return <div className="ms-2 me-auto">
            <div className="fw-bold">{song.name}</div>
            {song.by}
          </div>;
}

Viewing the App

Throughout all of this development, we were running the site almost constantly. We wanted a working site at all times, so that we could get quick feedback on whether any individual change is working. Never write huge amounts of code without verifying that the code works. A classic beginner mistake is to “finish” the site without actually running it - you will often find that your work has only just begun, since now you must debug everything.

Criticizing our own workflow, we could have been more consistent about making small, regular commits. The history shown on GitHub does not do justice to the amount of deleting and experimenting that we did along the way. Commiting regularly requires discipline, and may feel slow, but usually pays off when you are working in a large team.

Normally, at this point, we would write some tests to verify that our site loads and displays the correct information, without us needing to actually run and test the site by hand. Unfortunately, we’re short on time in developing this chapter, so have to move on to the interactive features.

Messing with State

We ended up creating several helper functions to streamline how we manipulated the movies’ state. Notice how these functions are implemented as a closure over movies and setMovies (the variables are used inside the nested functions since they are availabel from the enclosing scope). An alternative approach would have been to define helper functions that live in their own separate file (similar to what we did for arrays.ts, objects.ts, and nested.ts), which would require us to pass around movies and setMovies to any component that wished to use them. There are advantages to providing a reduced interface, but also tradeoffs to keeping code localized to where the code is needed - you have to make think about performance and readability of your code.

A common trick when you have a lot of helper functions is to define a special object that can hold the methods. We will see more sophisticated versions of this if we ever get to finish the bonus chapter on useReduce. But for now, know that there are tricks to consolidate the number of helper functions that have to be passed around into a single object.

Watching a Movie

The first bit of interactivity was relatively simple: we wanted to let users click buttons to indicate that they have watched and/or liked a movie. We started with our previously-mentioned helper functions up in App (since that was where we defined setMovies). Eventually, we used those helper functions in a new component named RecordControls (because it lets us “control” the “movie records”, not because we were recording - perhaps not a great component name if we need to explain it!).

The new Date().toLocaleString() expression is used to get the current date and time as a pretty string. Although we store this data, we never actually ended up using it in our app. Premature optimization has made fools of us!

console.log(new Date().toLocaleString());

Only the setMovieWatched function needed to be passed down the component hierarchy, because the watchMovie is already safely embedded inside of it. However, that still means we are passing setMovieWatched down quite a few levels. You can observe how the Props of MovieList and MovieView component swell up with all these helper functions, when we really just wanted to pass it down to

Once we reach the level where we instantiate our new RecordControls, we do something curious: we create an anonymous function (lambda) based on our setMovieWatched function. This anonymous function performs a trick called “partial function application”. The idea sounds complicated, but the goal is to make life easier later on: we no longer need to pass in the movie or movie.id to RecordControls, just the setMovieWatched function on its own. You will sometimes hear this referred to as “binding” or “fixing” a parameter to the function.

Along with the setMovieWatched, we also need to provide the current movie.watched value, so the current state can be rendered. We’ll talk about the changeEditing parameter in the next subsection.

Taking a look at the RecordControls component, things seem pretty messy. We have two buttons to render, but two of the buttons are slightly different depending on the seen and liked fields. The code becomes a bit easier to read when you see that we are really just wrapping each button’s possible version in a ternary. This would have been a good chance to make a more general purpose helper component, in fact, to decompose the work being done in this component and make the code more readable. Each button is basically the same, if you study them closely.

Think carefully about how we started with the State and Controllers at the top of the heirarchy, passed the data down through the components, and then finally wrote most of the View in the bottom-most component. This is not necessarily ideal - the whole goal of components is to centralize the necessary State, Controllers, and View right where they are most needed. Still, this is still much better than if we tried to dump everything in App.

Editing a Movie

The next bit of interactivity is for users to be able to edit the title, description, rating, and release year of each movie. Each of these attributes will eventually correspond to its own Form:

  • Textbox for the title,
  • Textarea for the description,
  • Dropdown for the rating, and
  • Number box for the release year

However, the user should not see these Forms all the time, only when they are in “editing mode”. Each movie requires its own unique state to determine whether the movie is being edited. And fortunately, this state does not need to be used outside of the Movie’s personal MovieView component. That means we can keep the state inside of MovieView, without needing to lift the state up further. Therefore, we define editing and setEditing from useState inside of MovieView, along with a helper state setter function changeEditing that inverts the current state of editing.

The editing state variable controls whether we render the regular MovieView component with its nice human-friendly content, or the MovieEditor component. The MovieEditor component nicely encapsulates a lot of forms, and it probably would have been a good idea to do the same thing for the “regular” MovieView content: move that content into a separate helper component (perhaps MovieContent) so that MovieView’s only job was to conditionally render MovieEditor or this new MovieContent component.

The MovieEditor component is a long component that contains all the forms for editing the attributes of the Movie object. The Forms do not edit the main movies state directly: they instead manipulate local state variables that correspond to the eventual fields in the movies state variable. This “temporary” state is useful since users can write whatever they want, and then cancel their changes harmlessly. However, many applications will directly manipulate the data, which requires slightly different onChange bindings.

In order to activate and deactivate Edit Mode, we have buttons that call the changeEditing function. Whether we are in Edit Mode or not, we need to provide a button to switch modes (otherwise we’ll be trapped in one of the modes). The button to enter Edit mode is in RecordControls, and the buttons to escape Edit Mode are in MovieEditor. The escape buttons are broken up into Save and Cancel buttons; that way the user can abort making changes if they want.

Ultimately, we also need a way to update the specific movie in the main movies state variable. Back in App, we defined an editMovie helper function that wraps setMovies and uses the map function to conditionally replace the newMovie we created.

Editing a Song

The Movie’s soundtrack is a little more complicated to edit, since there are a list of songs for each movie. However, the model is largely the same as what we’ve seen before. We begin by declaring some state for the temporary soundtrack, at the MovieEditor level so we have access to soundtrack inside of the save helper function we described previously. We also need to instantiate the SoundtrackEditor component and provide the state.

We wrote SoundtrackEditor.tsx a little differently than some of the other components, to highlight some of the variation possible. Specifically, the file contains not only the SoundtrackEditor component, but also two helper components named SongByEditor and SongNameEditor. Usually, each component lives in a separate file, but some folks also believe it’s reasonable to place components near each other if there’s a strong relationship between them. And in this case, the SongNameEditor and SongByEditor are being exclusively used by the SoundtrackEditor component.

The main responsibility of the SoundtrackEditor component is to render the song list (using a ListGroup and map), with each individual Song rendered as the two helper components (SongNameEditor and SongByEditor) inside of a div. Each component is just a textbox, and require the same props, so we encapsulated the type definition of those props into a new interface named SongProps. Both components depend on a helper function named setSong that updates a given song based on its id field using the provided newSong parameter.

Deleting a Movie

Deleting a Movie only requires a new helper function and a new button in MovieEditor.

Adding a Movie

Unfortunately, adding a new movie is much more complex. The user must specify the ID of the Movie and the IDs of all the Songs. Otherwise, we cannot insert them into the list. A full version of the application would probably allow the user to write the other fields, but our minimal version only needs the IDs, so that is all we will require from the user.

We put the Forms for prompting the user about a new movie inside a Modal dialog box (popup) to keep things a little more organized. This requires an additional bit of state (showAddModal) at the App level to determine whether or not the Modal should be currently visible.

The fact that we have the AddMovieModal outside of the MovieList explains why our movies state variable was declared in App. Both of these components are direct children of App, and both components needed access to the state setter setMovie (albeit indirectly through their respective helper functions). Therefore, the state needed to be lifted very high to reach down to both.

The saveChanges function is really crying out to become a helper function. Although that would not save us much code, the readability would be dramatically improved, and we could unit test that functionality more easily.

The Movie’s ID field is just a textbox, since there is just one. However, we must be more careful with the song list, since we are trying to get a list of data. We rely on a new component named EditableSongList.

The EditableSongList component has several parts and depends on state created in AddMovieModal (the songs and setSongs state).

Testing the Application

We haven’t written any tests for this codebase yet. There’s no reason other than lack of time. Again, you should really be testing this application consistently throughout. Certainly, before we declare victory, we really should write some tests. Some example ideas that we will do when we have more time:

  • Does the page render the movies at all with all of the expected information?
  • Can we edit a movie’s name? Its description? Etc.
  • Can we delete a movie?
  • Can we add a movie with several songs?
  • Can we add a movie, delete that movie, and a new different movie?
  • Can we edit a song’s name?

Iterate

At this point, we have enough of a website that we should definitely show this version to a human. In fact, most likely, we should have been showing even earlier versions to the client throughout. For now, we are simply trying to get some practice in developing web applications, so we’re relaxing our usual process a lot. In the next chapter, we’ll talk more about the best practices for developing software in a maintainable way that leads to a successful product.

As we mentioned earlier, the example shown here may look like a straightforward line of development from A to B. However, the reality is that we kept changing things and undoing things. Programming is a messy process, and we often make mistakes: we don’t realize that some state needs to be higher, or that a helper function needed another parameter, or a Component could be broken up in several places. At least, not at first - these are easier things to realize after you’ve written them.

One of our last pieces of advice is to keep your codebase as clean as possible as long as you can. Cutting corners, abusing state, and not thinking about how to break things up may often seem convenient. But you pay the “technical debt” you accumulate at some point - often near the end of the development, when you encounter bugs or issues and you can’t fix them because you chose poor variable names or aren’t decomposing.

đź“ť Task - Quizzer

The task for this chapter is very different from previous ones. Up until now, we had the expectation that you would be able to achieve every subtask we set out, and that you would not proceed without finishing each part. However, that expectation might be unrealistic for this Task since we are establishing a lot of potential critiera. Your grade will be influenced by the amount of criteria that you complete, but we do not really expect you to finish all of the task 100%. Use this opportunity to learn what you can about writing a larger application!

Our goal is to have you walk through the same process we did in developing the Movie Records, but instead developing an application for quizzes (both academic ones and fun trivia ones). We have already created a list of user requirements below. However, we have not sketched the application, started any of the development, or written you any tests. These are all things that we want YOU to do.

Create the Branch

We begin by having you checkout the solved-forms branch, so that we can then branch off into a new fresh branch.

$> git checkout solved-forms
$> git pull upstream main
$> git fetch upstream task-quizzer
$> git checkout -b solved-quizzer
$> git merge upstream/task-quizzer

You will find that we have provided VERY little code. It’s really just the quizzer folder, the quizzer/Quizzer.tsx file, and the quizzer/Quizzer.test.tsx file. We expect you to add more files in order to achieve the final result.

Task Requirements

Here is our set of user requirements that we have solicited from our client.

  • Users can see a list of quizzes, including the quizzes title, description, and how many questions it has
  • Users can select a specific quiz to see the questions, including the question’s name, body, and points
  • Quiz questions can be of AT LEAST two types: a short answer question or multiple choice question
  • Users can enter or choose an answer for a quiz question, and be told if they are correct
  • Users can see how many total points they have earned
  • Users can clear out their existing answers for a quiz
  • Users can publish or unpublish a question
  • Users can filter the questions in a list so that only published questions are shown
  • Users can edit the questions and fields of a quiz
  • Users can add a new quiz question
  • Users can delete an existing quiz question
  • Users can reorder quiz questions
  • Users can add a new quiz
  • Users can delete an existing quiz

If you need clarification for a requirement, you can ask the client. But in general, the policy is that if the requirement is vague or behavior is not required, you should feel free to make the call on this. The goal is to get you practice making a larger-scale application, not to make a real piece of software. For now!

First Steps

Going through our advice again, you will hopefully see the general flow of how we worked:

  1. Gather requirements
  2. Sketch the application
  3. Develop the data model
  4. Develop some test data
  5. Build a “Minimally Viable” version
    1. Get the data to render
    2. Add interactivity

We strongly urge you to proceed in this fashion. Part of our grading will be to look at your sketch and data model!

Existing Code

If the components from the other tasks are getting in your way, you can hide them behind some conditional logic of a new component: either a Show/Hide kind of component, or a Mode Switch component. The idea for the Show/Hide component is as follows:

  1. Take all of the existing component instantiations from App and move them into the new component
  2. Add a new bit of state (e.g., a boolean state named visible)
  3. Render a button to invert the state
  4. Use the && operator to only render the existing components when the state is true.
// Just an example, many other ways to do this!
export function ShowHideTasks(): JSX.Element {
  const [visible, setVisible] = useState<boolean>(false);
  return <div>
    {visible && <div>
        // Existing component instantiations go here
        ...
    </div>}
    <Button onClick={()=>setVisible(!visible)}>Show/Hide</Button>
  </div>;
}

// App.tsx
export function App(): JSX.Element {
  return <div>
    ... Quizzer component ...
    <ShowHideTasks></ShowHideTasks>
  </div>;
}

The Mode Switch component would work similarly, except with a String state (e.g., “quiz mode”, “earlier tasks mode”) and the ? operator. Ultimately, how you want to hide the earlier tasks is up to you, as long as we have easy access for grading purposes! Make sure your button is down near the bottom of the page so that we can find it.

None of this should affect your existing tests, since they work independently of what happens in the App component itself.

Grading

Given the open-ended nature of this application, you are being graded by a rubric. Completing more requirements will tend to get you more points, but so will writing tests. You also earn points for things like sketching the application and making up test data.

Everyone will feel comfortable completing a different number of requirements, and have different amounts of time they can commit. We understand how this can be stressful and we appreciate your flexibility in approaching this activity the right way. Recognize that you can pass the course even if you are struggling to get started - you will be getting another attempt when we get to he Final Project, and there you will explicitly have a team, so things are likely to go more smoothly.

Sketch

If you produce a sketch (and you should), then embed the sketch somewhere on your webpage BELOW the Quizzer as an image.

Completing Requirements

Every time you complete a requirement, add them to a bulleted list of “Completed Features” that we can see on your website BELOW the Quizzer and Sketch. Don’t forget to keep this list updated!

Submission

Soon, we will want to review your application so that we can make decisions about team assignments. Do not delay in submitting for this Task - as soon as you have started, deploy your application and submit on Canvas.

$> git push --set-upstream origin solved-quizzer

As you make more changes, add and commit regularly. Every now and then, you can push your changes (no longer needing to specify the upstream):

$> git push

Failure to submit this assignment tells us something about your preparation for the Final Project. There will be more time before the Final Project begins to work on this Task, but do not prevent us from viewing your application!

Creative Freedom

You have a LOT of freedom to make the Quizzer look the way you want. Colors, spacing, specific features - we want to see folks have fun and build something they feel they can own.

Work together, show people your design, and avoid living in a vaccuum. Make a fun trivia quiz to show to your classmate and have them try out your application! Make the entire application dog-themed! The sky is the limit, as long as it meets the general requirements we specified before.


Back to top

Created by Austin Cory Bart, Josh Lyon, Kurt Hammen, Emma Adelmann, Terry Harvey.