Connect the User Interface

Our interface will look like this:

images/redux_ui.png

Below each header, we list the movies for that day. While the movies load, we’ll display a loading indicator:

images/movies_loading.png

Some of our components just display the props they receive from their parent, while others talk to the store. To connect the components to the store, we’ll use the React Redux library. The advantage of React Redux is that it allows you to transition an existing application to Redux without rewriting most of your components.

The way React Redux works is subtle, so let’s break it down. Our React components need to read data from the store so they know what to render. They also need to call the store dispatch to send actions to the store, and to subscribe to the store so they know when data has changed. Without React Redux, we would have to repeat this logic for each component that wants to talk to the store. React Redux provides a function called connect. It creates new wrapper component classes, which you’ll sometimes hear people call “higher order components.” These handle the process of subscribing to the store, extracting the data your own component wants, and re-rendering your component when the store updates.

Our job will be to create presentational components, then call connect on them to create the wrapper components that talk to the store. Let’s start with the Checkbox component that we’ll use both for the filter toggle and the favorite checkbox. In the src directory, create a new file named Checkbox.js and define a function component named Checkbox:

 import​ React from ​'react'​;
 
 export​ ​default​ ​function​ Checkbox({ checked, onChange, name, label, id }) {
 function​ onCheck(event) {
  onChange(event.target.checked);
  }
 
 return​ (
  <div className=​"flex mw4"​>
  <label className=​"pr2"​ htmlFor={id}>{label}<​/label​​>
  <input
  type=​"checkbox"
  name={name}
  id={id}
  onChange={onCheck}
  checked={checked}
 /​​>
  <​/div​​>
  );
 }

The checked prop indicates whether the checkbox should be checked. onChange is an event handler to call when the checkbox checked state changes, and name, label, and id improve accessibility by allowing us to create a label.

This returns an <input> tag with its label. We set the <input> type to checkbox. onChecked calls the onChange prop with the current checkbox value when the checkbox value changes.

Once we have the Checkbox component, we can immediately create the filter that will toggle whether only favorite movies are displayed. The filter reducer creates a filter boolean property: when filter is true, only favorite movies are displayed. We need to pass filter as the checked props. The filter reducer returns a new filter value when the store receives the FILTER_CHANGED action, so we will dispatch the FILTER_CHANGED action when the checkbox value changes.

To get started, install the react-redux package:

 $ ​​npm​​ ​​i​​ ​​--save​​ ​​react-redux

In the src directory, create a new file named Filter.js. Import connect from react-redux, the FILTER_CHANGED action constant, and finally your own Checkbox component:

 import​ { connect } from ​'react-redux'​;
 import​ { FILTER_CHANGED } from ​'./actions'​;
 import​ Checkbox from ​'./Checkbox'​;

To pass the store state filter property as the checked prop, define a function named mapStateToProps:

 function​ mapStateToProps(state) {
 return​ {
  checked: state.filter
  };
 }

mapStateToProps receives the current store state. It returns an object. React Redux passes the object as props to Checkbox. To map the state filter property to the checked prop, use state.filter as the value of the checked key.

Next, dispatch the FILTER_CHANGED action when Checkbox calls onChange. Define a new function, mapDispatchToProps:

 function​ mapDispatchToProps(dispatch) {
 return​ {
  onChange: filter => {
  dispatch({ type: FILTER_CHANGED, filter });
  }
  };
 }

mapDispatchToProps receives the store dispatch function as an argument. The onChange prop receives the current checkbox value as a boolean. Create a function that takes a boolean and dispatches the FILTER_CHANGED action with the filter property set to the boolean argument. Pass this function as the onChange prop. If you create the same action in multiple places across the application, you can extract the action creation code into its own functions called action creators.

Finally, create the wrapper component that talks to the store:

 export​ ​default​ connect(mapStateToProps, mapDispatchToProps)(Checkbox);

The connect is a bit weird. It returns a function that returns a component. With the first pair of parentheses, we’re calling connect itself. We pass mapStateToProps and mapDispatchToProps. This returns a function that we can immediately call on the Checkbox component with the second pair of parentheses. The whole thing returns a new component, which renders Checkbox with the props you defined in mapStateToProps and mapDispatchToProps. We export this new component so we can use it in the main UI.

Now let’s render the filter. In index.js, insert the filter in the UI. Import React, ReactDOM, and the Provider component from React Redux, as well as your own Filter component. To pass the store to the filter component, wrap the whole interface in the Provider component and set the Provider store prop to the store. This will make the store available to all components created with connect without the need to pass it yourself. In ReactDOM.render, render the filter component. Pass the control name, id, and label as props:

 import​ React from ​'react'​;
 import​ ReactDOM from ​'react-dom'​;
 import​ { Provider } from ​'react-redux'​;
 import​ store from ​'./store'​;
 import​ { requestMovies } from ​'./movieApi'​;
 import​ { MOVIES_LOADED } from ​'./actions'​;
 import​ Filter from ​'./Filter'​;
 
 requestMovies().then(movies => store.dispatch({ type: MOVIES_LOADED, movies }));
 ReactDOM.render(
  <Provider store=​{​store​}​>
  <main className=​"ph6 pv4"​>
  <h1 className=​"mt0"​>Programme</h1>
  <Filter name=​"filter"​ id=​"filter"​ label=​"Just favorites"​ />
  </main>
  </Provider>,
  document.getElementById(​'app'​)
 );

To serve the application, make sure that index.html contains a <div> with an id of app. Include Tachyons for styling:

  <link rel=​"stylesheet"
  href=​"https://unpkg.com/[email protected]/css/tachyons.min.css"​/>
 </head>
 <body class=​"sans-serif"​>
  <div id=​"app"​></div>

If webpack-dev-server still runs, reload the page. Otherwise, restart webpack-dev-server with npm start. When you visit the page, the checkbox appears:

images/favorites_check.png

Check and uncheck the checkbox a few times, then, with the devtools, inspect the actions you generated. Observe both the dispatched actions and the state snapshots. The FILTER_CHANGED appears in the action list, while the state diff shows the filter property switching between true and false.

We’re done with the filter. Let’s tackle displaying the movies. We’ll build the loading indicator, the movie box, and the list of movie boxes.

Let’s start with the loading indicator. Like the filter property, we have a single loading boolean in the store state that indicates whether the movies are still loading or not. The loading indicator component takes a boolean loading prop and a list of children. If loading is true, it displays the children; otherwise, it displays the “Loading…” message. Create the LoadingIndicator component in a file named LoadingIndicator in the src directory:

 import​ React from ​'react'​;
 import​ { connect } from ​'react-redux'​;
 
 function​ LoadingIndicator({ loading, children }) {
 if​ (loading) {
 return​ <div>Loading…</div>;
  } ​else​ {
 return​ (
  <div>
 {​children​}
  </div>
  );
  }
 }

React passes the children prop implicitly every time an element is a parent of other elements.

Connect the loading prop to the loading property in the store state. In LoadingIndicator.js, create a new mapStateToProps function:

 function​ mapStateToProps(state) {
 return​ {
  loading: state.loading
  };
 }

As before, the state parameter represents the store state. We set the loading property to state.loading to pass the loading store state property as the loading prop.

Finally, create the wrapper component. Since we don’t need to dispatch any actions, we use connect, which we imported from React Redux, and pass just mapStateToProps and LoadingIndicator and export the result:

 export​ ​default​ connect(mapStateToProps)(LoadingIndicator);

Next, create the box to display a single movie. Each box displays the movie title and a checkbox. When the user checks the checkbox, dispatch the FAVORITED action to the store, and dispatch the UNFAVORITED action when the user unchecks the checkbox. In the src directory, create a new file named MovieBox.js and define a new MovieBox component:

 function​ MovieBox({ movie, favorite, onFavorite, onUnfavorite }) {
 function​ onChange(checked) {
 if​ (checked) {
  onFavorite(movie.title);
  } ​else​ {
  onUnfavorite(movie.title);
  }
  }
 
 return​ (
  <div className=​"h4 mt3 pa3 flex flex-column justify-between ba b--dashed"​>
  <h3 className=​"mt0 mb3"​>​{​movie.title​}​</h3>
  <Checkbox
  name=​"addToFavorite"
  id=​"addToFavorite"
  label=​"Favorite"
  checked=​{​favorite​}
  onChange=​{​onChange​}
  />
  </div>
  );
 }

The movie prop describes the movie to show, favorited indicates whether the user has already favorited the movie, onFavorite is a function to call when the user favorites a movie, and onUnfavorite is a function to call when the user unfavorites a movie. For the interface, import Checkbox. Display the movie title in a header, next to a checkbox with the “Favorite” label. Check the checkbox when favorite is true. Pass onChange as the onChange prop to Checkbox. onChange receives the checkbox state as a boolean. If the checkbox is checked, call onFavorite with the movie id to signal that the user favorited the current movie; else, call onUnfavorite.

Next, we’ll use connect to construct a new component that receives the movies directly from the store and dispatches the FAVORITED and UNFAVORITED actions when the user interacts with the checkbox.

Import the connect function and the FAVORITED and UNFAVORITED action constants at the top of MovieBox.js:

 import​ React from ​'react'​;
»import​ { connect } from ​'react-redux'​;
 import​ Checkbox from ​'./Checkbox'​;
»import​ { FAVORITED, UNFAVORITED } from ​'./actions'​;

Let’s start by mapping the store state to the MovieBox props. The parent component will pass the movie itself, so we only need to pass the favorited state from the store:

 function​ mapStateToProps(state, ownProps) {
 return​ {
  favorite: state.favorites.includes(ownProps.movie.title)
  };
 }

The second parameter to mapStateToProps represents the props you pass to the component returned by connect. To determine whether the movie is favorited, check that the array of favorited movies includes the movie that MovieBox displays. The includes array function returns true if the array contains the item you pass to includes.

Next, let’s map dispatch. Create another function named mapDispatchToProps:

 function​ mapDispatchToProps(dispatch) {
 return​ {
  onFavorite: movieId => dispatch({ type: FAVORITED, movieId }),
  onUnfavorite: movieId => dispatch({ type: UNFAVORITED, movieId })
  };
 }

We pass a function that dispatches the FAVORITED action as the onFavorite prop and a function that dispatches the UNFAVORITE action as the onUnfavorite prop.

Finally, create the wrapper component. Pass mapStateToProps and mapDispatchToProps to connect and export the result by default:

 export​ ​default​ connect(mapStateToProps, mapDispatchToProps)(MovieBox);

Finally, create the component that lists the movie boxes. In the src directory, create a new file named MovieList.js. The MovieList component displays either a list of movies or the loading indicator. Import MovieBox and LoadingIndicator. The movies prop is an array of movie objects. For each movie object, create a MovieBox element. Assign the MovieBox list to movieBoxes. Place the movie date in the header. Wrap a LoadingIndicator element around the MovieBox list:

 import​ React from ​'react'​;
 import​ { connect } from ​'react-redux'​;
 import​ MovieBox from ​'./MovieBox'​;
 import​ LoadingIndicator from ​'./LoadingIndicator'​;
 
 function​ MovieList({ movies, date }) {
 const​ movieBoxes = movies.map(movie =>
  <li key=​{​movie.title​}​><MovieBox movie=​{​movie​}​ /></li>
  );
 return​ (
  <div className=​"w5 pr3"​>
  <h2>​{​date​}​</h2>
  <LoadingIndicator>
  <ol className=​"list pa0"​>
 {​movieBoxes​}
  </ol>
  </LoadingIndicator>
  </div>
  );
 }

You already connected LoadingIndicator to the store, so that it displays the loading message when the store state loading property is true.

Create mapStateToProps to retrieve the movies props from the movies property in the store state. The movies to display depend on the filter; if state.filter is true, pass only the favorited movies to the component; else, pass all movies. You also need to filter the movies by date. Access the date for the current MovieList element from ownProps.date:

 function​ mapStateToProps(state, ownProps) {
 if​ (state.filter) {
 const​ activeMovies = state.movies.filter(movie =>
  state.favorites.includes(movie.title)
  );
 return​ {
  movies: activeMovies.filter(movie => movie.date === ownProps.date)
  };
  } ​else​ {
 return​ {
  movies: state.movies.filter(movie => movie.date === ownProps.date)
  };
  }
 }

Finally, pass mapStateToProps and MovieList to connect and export the result:

 export​ ​default​ connect(mapStateToProps)(MovieList);

Let’s render the remaining part of the interface. Open index.js again and import MovieList. For each day, create a MovieList element and pass the day as the date prop. Each MovieList element needs a unique key prop so that React can render it correctly. Use the day for that. map returns an array of MovieList elements. Assign it to the movieLists variable, then render each MovieList by placing the whole array inside braces in the JSX structure:

 import​ React from ​'react'​;
 import​ ReactDOM from ​'react-dom'​;
 import​ { Provider } from ​'react-redux'​;
 import​ store from ​'./store'​;
 import​ { requestMovies } from ​'./movieApi'​;
 import​ { MOVIES_LOADED } from ​'./actions'​;
»import​ MovieList from ​'./MovieList'​;
 import​ Filter from ​'./Filter'​;
 
 requestMovies().then(movies => store.dispatch({ type: MOVIES_LOADED, movies }));
»const​ movieLists = [​'Monday'​, ​'Tuesday'​].map(date =>
» <MovieList key=​{​date​}​ date=​{​date​}​ />
»);
 ReactDOM.render(
  <Provider store=​{​store​}​>
  <main className=​"ph6 pv4"​>
  <h1 className=​"mt0"​>Programme</h1>
  <Filter name=​"filter"​ id=​"filter"​ label=​"Just favorites"​ />
» <div className=​"flex flex-row"​>
»{​movieLists​}
» </div>
  </main>
  </Provider>,
  document.getElementById(​'app'​)
 );

Visit the page again, and check that everything works. As you click the filter initially, all of the movies disappear, but if you select one of the movies, it remains. If something misbehaves, check that the FAVORITED and UNFAVORITED actions appear in the Redux devtools. You can also look at the state snapshots and check that the movie titles get added to the favorites.

If you want to develop this application further, you could try extracting action creators and selectors in their own functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.51.228