CS 312 - Software Development

Lecture 6 - React Design

Film Explorer, Container components, and Immutability

Exploring the Film Explorer

Experiment with a simple Film Explorer application and explore its code. After implementing the Color Picker with PropTypes and CSS Modules, the components of the Film Explorer should look familiar.

Note the key property in the "list" of films in the FilmTable (we saw this in our assignment as well). The key property uniquely identifies element in a list (to speedup rendering by identifying which specific elements have changed). From the React documentation: "A good rule of thumb is that elements inside the map() call need keys."

Container components

Exploring the demo, we observe that the films are sorted (and filterable) and can be "clicked" to show more detail (the poster and overview). We could so as part of the FilmTable and FilmSummary, but we would like to separate the logic and UI (recall "Separation of Concerns"). We can do so by introducing a "container component" (CC). A CC is not a thing per-se, it is a design pattern.

A CC is concerned with "how the application" works and thus implements logic, is often stateful, but does not typically generate DOM (HTMl elements). Its counterpart is the "presentation component" (PC). A PC is focused on how the application looks and typically generates styled DOM but does not fetch or manipulate data. A CC will typically implement some logic, passing the result of that computation (pure or stateful) to children components (which may be PCs or more CCs) to be displayed.

For the Film Explorer we can extract two container components:

  • FilmTableComponent: Implements the film sorting (and eventual filtering), passing the order films as a prop to the FilmTable presentation component
  • FilmComponent: Implements the switching between summary and detail views

Immutability

Since React works by re-rendering on any state change, it is important for it to be aware that state has actually changed. The first piece of that is to only use our state setters. With primitive values, this is fairly robust. Consider the FilmContainer:

function FilmContainer(props) {
  const [showDetail, setShowDetail] = useState(false);
  const View = showDetail ? MovieDetail : FilmSummary;
  return (
    <View
      {...props}
      onClick={() => {
        setShowDetail(!showDetail);
      }}
    />
  );
}

We are storing a Boolean value, and when we request our variable we make it a const, so we will get complaints from the interpreter if we try to write into it directly. The less obvious piece is what happens when we call the setter. React tries to be intelligent and not re-render if it doesn't have to, so it will check to see if the value is actually a new one. With a primitive value, this is just a simple equality check.

Things get more complex with objects, arrays, and other data structures (okay, they are all objects). These can be declared constant, but that only means that the reference to the memory location stays constant. The stored value can be changed. This is legal:

const obj = {a:1, b:2};
obj.a = 5;

This has a couple of problems. First, it means we can accidentally change state when React isn't looking. Second, even if we are careful and pass the modified object back to the setter, React will think nothing has changed because the reference is the same. It won't do a "deep" equality check, and even if it did, we just changed React's "copy" as well, because React just has a reference to the same object (it may not say "pointer" anywhere, but understanding how they work is important in all languages...). So what can we do?

If FilmExplorer we have two examples. One approach is to be very cautious and copy any object when we are going to make a change. Here is an example for when we set the rating of a film:

 const setRating = (filmid, rating) => {
    const alteredFilms = films.map((film) => {
      if (film.id === filmid) {
        return { ...film, rating };
      }
      return film;
    });
    setFilms(alteredFilms);
  };

This actually provides two examples because we are changing two things: we are changing the rating of a single film, but the actual state variable is the whole list of films. If we just changed the film itself, we would be modifying state (the list films), and that change would be invisible even if we passed films to setFilms() since the reference wouldn't have changed. So, you can see that we are using map(), which generates a brand new array with the results of all of the individual function calls. Of course, our function merely returns the original items for all films except for the one we are changing. For the film itself, we use the spread operator to make a new object to replace the old copy. With care, this technique of making copies allows us to treat complex objects as if they were immutable.

Unsurprisingly, there are a variety of libraries that provide you with actual immutable data structures that enforce the "new copy on change", all with different approaches and different mechanisms to make the process or or less transparent.

We won't be using any of these libraries this semester in the interest of putting some limit on the number of technologies you need to master. However, you should be aware they are out there. It is more important, however, that you just avoid mutating state or props in the first place.

The big picture:

  • Don't mutate values you are using for state or props
  • Primitive data types should be favored
  • To make state update pure, replace instead of modify
  • If performance becomes an issue, or you have deeply nested state objects, try using immutable data structures like those in immutable.js