Stop any project you currently have running; let's go back to the film application that you've started. You can run the app with npm start.
You're almost finished! Now, you need to:
- Add films to a user's faves
- Filter the films the user is looking at
To do this, you'll need to move your state up to the top of the component tree so all of the data is more easily shared across components. Remember unidirectional flow - data is going to go down the component tree, so you'll want our states as high as possible.
Your App component will be the main place where states are set. From here, these states can be passed as props down to the other components.
To have access to state, we need to refactor App.js into a proper class Component. It should look like this:
import React, { Component } from 'react';
import './App.css';
import FilmDetails from './FilmDetails.js';
import FilmListing from './FilmListing.js';
import TMDB from './TMDB.js';
class App extends Component {
render() {
return (
<div className="App" >
<div className="film-library">
<FilmListing films={TMDB.films} />
<FilmDetails films={TMDB.films} />
</div>
</div>
);
}
}
export default App;Now, set three states:
films: initialize this key to hold a reference toTMDB.filmsfaves: this key should simply start off as an empty arraycurrent: this key should start off as an empty object
Now that you have state stored on the App component, you want to pass those as props to your child components. Just change App.js for now:
-
FilmListingshould receive afilmsprop that now references your state'sfilmsproperty and afavesprop that should reference your state'sfavesproperty. -
FilmDetailsshould receive afilmprop that references your state'scurrentproperty.
Since you aren't doing anything with these props yet, nothing should change.
When a user favorites a film, that information needs to be shared with the rest of your components.
- For example,
FilmListingneeds to know which films are favorites to enable the filter.
This isn't possible right now, because you're currently handling the favorite toggling of a film on the Fave component. The Fave component is at the bottom of the component hierarchy, and props and state only flow downward. Additionally, the Fave component doesn't even know which film is being favorited, so this isn't a great place to store a state for whether a film is a favorite.
Let's fix this:
Take the isFave state out of the Fave constructor.
Since you're no longer holding the state in the Fave component, you no longer want to set the isFave state in the handleClick() event handler.
Instead, assume that the parent component will pass a handler called onFaveToggle to you through the props object.
Change handleClick as follows:
# /src/Fave.js
handleClick(e) {
e.stopPropagation()
console.log('Handling Fave click!')
// Add this line. You'll call the function passed through props
this.props.onFaveToggle()
// Delete the `setState` line. You no longer track state here
// this.setState({isFave: !this.state.isFave})
}This way, when a user clicks, onFaveToggle will be called at a higher component level.
You've taken the isFave state out of Fave and will be passing a prop called isFave instead. In the Fave component, replace this.state.isFave with this.props.isFave. You'll send that information down from a parent component that knows this info.
This is all you need to change in Fave.js! It will still check to see if the user has clicked the fave toggle button. The difference is that once the user clicks, instead of changing the faves array directly, the handleClick function will instead call onFaveToggle to do it instead.
You'll define onFaveToggle in a higher component.
The Fave component is expecting a prop, but one doesn't exist yet. Let's change that next.
You'll move the favorite toggle functionality all the way up to the App component - where the state for films and faves is stored.
- In the
Appcomponent, create ahandleFaveToggle()function. It doesn't need to do anything yet, but soon you will update thefavesarray when a film is favorited or unfavorited. ThehandleFaveTogglefunction should accept a film object as an argument (this will be the film that the user is toggling).
As you saw previously, you need to bind your custom component methods to ensure this refers to the component within the body of the method.
Add the following to the App component's constructor:
this.handleFaveToggle = this.handleFaveToggle.bind(this)To recap, the faves state is going to hold the user's favorite films. Your goal is to, when the user clicks the icon to favorite or unfavorite a film, either add or remove the given film from the faves array.
To do this, you need to call setState and give it the updated array (you can't just update it directly; otherwise React won't know to re-render the components to reflect the changes). To accomplish this, you'll make a copy of the existing faves array, update it, then pass the copy to setState.
First, just make a copy. Inside handleFaveToggle, use the JavaScript Array.prototype.slice() method to make a copy and store it in a const variable called faves.
Now underneath the slice, use the JavaScript Array.prototype.indexOf() method to store the position of the film in the array in a const variable called filmIndex.
Now, filmIndex will be an index value starting at 0.
If the film is found in the array, indexOf() will return an index value starting at 0. Conversely, indexOf() will return -1 if the element isn't found - if the film it's looking for is not currently in the faves array.
Since this handleFaveToggle() function is designed to change the array of the user's favorites film, there are two options.
- If the film is already in their favorites, then when the user clicks the button, they want to remove it from their favorites. You need to take it out of the
favesarray. - If the film is not in their favorites, then when the user clicks the button, they want to add it to their favorites. You need to add it to the
favesarray.
Write a conditional statement with the two cases. When adding a film to faves, log out Adding [FILM NAME] to faves... and when removing a film from faves, log out Removing [FILM NAME] from faves....
To remove a film that's already in the faves array, use the Array.prototype.splice() method.
To add a new film to the faves array, just push it on to the end of the array.
Now that you have updated the faves array, you need to call setState so React will re-render the appropriate components in the tree. You can make this very succinct by using object literal shorthand. It should look like this:
this.setState({faves})
// The above is exactly the same as this.setState({faves: faves})Now that the handleFaveToggle method lives on the App component, you want to pass it all the way down the tree so that you can call it when the "Fave" button is clicked.
In the App component's render method, add a new prop to the FilmListing component called onFaveToggle. Its value should be a reference to the handleFaveToggle method you just finished writing.
In the FilmListing component, you render one FilmRow component for each film in the films prop. You need to pass the onFaveToggle function down to each FilmRow (with the ultimate goal being that you call it in the Fave component), but you want to ensure that it passes the current film up to the handleFaveToggle method in the App component when called.
To make this happen, you won't simply pass the function down to FilmRow as a prop as-is; you'll wrap it in another function that simply calls the onFaveToggle function passed down from App through props (remember, onFaveToggle in FilmListing is just a reference to handleFaveToggle in the App component).
In the FilmListing component's render method, add the onFaveToggle variable. Replace your existing map function with this:
const allFilms = this.props.films.map((film) => {
return (
<FilmRow
film={film}
key={film.id}
onFaveToggle={() => this.props.onFaveToggle(film)}
/>
)
})Now, the FilmRow component is receiving the onFaveToggle function as a prop. However, the FilmRow component doesn't need the function - the Fave component does. You'll pass it along as a prop toFave.
In FilmRow's render function, where you call the Fave component, add a prop called onFaveToggle and pass it the onFaveToggle prop that FilmRow received.
Great! The onFaveToggle function is now being passed from the App component, where it's defined, down to the FilmListing component, down to the FilmRow component, down to the Fave component.
The Fave component is also expecting to receive a prop called isFave, so you need to pass isFave to the Fave component from FilmRow.
FilmRow doesn't know about the faves array, but its parent, FilmListing, does.
The isFave prop should be true or false depending on whether the film is in the faves array.
In FilmListing, when creating each FilmRow, pass a prop called isFave whose value uses the Array.prototype.includes() method to determine if the film is in the faves array or not.
Now FilmRow is getting the isFave prop, but it doesn't need it. It only needs to pass it along. In FilmRow, pass that prop through to Fave.
Now, Fave is getting the true or false boolean of whether a film is a favorite (isFave), as well as being passed the favorite toggle function (onFaveToggle).
Fave has everything it needs!
Look in your browser to see this working - the JavaScript console will log if something is added or removed from the user's favorites.
Currently in the browser, the faves counter the user sees is always 0. You'll update the counter in the FilmListing to accurately show the number of faves in the array.
If you look at what's rendered in the FilmListing component, right now the faves counter is hardcoded to 0. Replace that with the length of the faves array that is received through the props.
You now have favorites properly stored and available to all components, and you have a counter that accurately reflects that to the user.
Great job! Check it out in your browser.
In FilmRow, there's still the function to handle when a user clicks a row for more details.
Following the same steps as you did for the Fave event handler, move the handleDetailsClick definition to the App component.
For handleDetailsClick in the App component, just log to the console and set the current state to the passed film for now. You'll handle looking up film details later. Make sure you pass the current state as a film prop to the FilmDetails component.
You have the filter state on FilmListing, but you still need to make it actually change the UI. You're not going to move the filter state because this filter only affects the FilmListing, not any other parts of the app.
Add a conditional in FilmListing so that if the filter state is set to faves, the listing only shows films in the faves array. Otherwise, it shows all films.
Try it out - you should be able to add films to your favorites and view just your favorites list by clicking that tab.


