Billedresultat for react redux Exercise: RTK Query part 1 (MovieDB)

Idea: Introduction to RTK query (Thunk).
Background: https://redux-toolkit.js.org/introduction/getting-started, Usage Guide, https://redux-toolkit.js.org/rtk-query/api/createApi og https://redux-toolkit.js.org/rtk-query/api/created-api/overview

 

I denne opgave skal der laves en lille React-Redux App baseret på Redux-Toolkit Query (RTK Query). Applikationen vil benytte CreateApi til konfigurering af endpoints til TheMovieDB (3'parts webAPI), ConfigStore til konfigurering af Redux Store samt createSlice til at oprette Slices til storen. Der skal desuden anvendes Hooks (useSelector, useDispatch), reducers og Array-helper metoderne filter og reduce. Der benyttes desuden Bootstrap til styling.

Appen der skal udvikles skal se således ud:


Det er en lille applikation der skal demonstrere hvordan der kan hentes og vises informationer om film via TheMovieDb: https://www.themoviedb.org/. Der søges om en API-key her: https://www.themoviedb.org/documentation/api


Step 1 (Opstart - installation af Node pakker )
Opret et nyt projekt med kommandoen: npx create-react-app moviedb --template redux'' i din ReactRedux mappen (eller hvor du ellers finder det passende at have dit nye projekt).
Naviger til mappen 'cd moviedb' og kør 'npm start' for at afprøve at installationen er gået godt.

Når du har verificeret at appen køre slettes hele indholdet af src-mappen.

Step 2 (App.js og index.js)
Opret en ny fil i src-mappen: App.js - filen skal indeholde:

function App() {
  return (
    <div>
      <h1 className="title is-2">Movies from TheMovieDB</h1>
    </div>
  );
}
export default App;

Opret tilsvarende filen index.js med følgende indhold:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const el = document.getElementById('root');
const root = createRoot(el);

root.render(<App />);


Verificer at du får vist "Movies from TheMovieDB" i browseren


Vi har nu fået "boilerplate" koden på plads og kan begynde at implementere vores API Slice: moviesApi der skal definere endpoints til TheMovieDB.




Step 3 (store/apis/moviesApi.js)
Opret en ny mappe store med endnu en mappe apis der indeholder filen movieApi.js:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const moviesApi = createApi({
  reducerPath: 'movies',
  baseQuery: fetchBaseQuery({
    baseUrl: 'http://api.themoviedb.org/3/'
  }),
  endpoints(builder) {
    return {
      fetchPopularMovies: builder.query({
        query: () => {
          return {
            url: 'discover/movie',
            params: {
              sort_by: 'popularity.desc',
              api_key: '81c50c197b83129dd4fc387ca6c8c323'
            },
            method: 'GET',
          };
        },
      })
    };
  },
});

export const {useFetchPopularMoviesQuery} = moviesApi;
export { moviesApi };

Gennemgå og forstå koden.

Hint: createApi definere et set af 'endpoints' der beskriver hvordan data kan hentes (fetch/query) via GET-Request eller transformeres (mutate fx add/remove) via POST-request mm.
'reducerPath' specificere hvor state skal gemmes i Storen, 'movies' bliver "key" i storen. 'fetchBaseQuery' er en funktion der returnere en prækonfigureret version af den indbyggede 'fetch'.
URL'en til det GET-request der skal sendes til TheMovieDB, ser således ud: 'http://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=81c50c197b83129dd4fc387ca6c8c323' - det beskrives via baseUrl + url + params

Bemærk: Du må meget gerne anmode om din egen api_key fra themoviedb via:
https://developers.themoviedb.org/3/getting-started/introduction, istedet for bare at benytte min! (ok til lige at teste om der er hul igennem)

Bemærk: createApi
er en funktion der returnerer en API Slice til storen. API Slices indeholder Redux logik til at interagere med server endpoints og indeholder Reducer til at håndtere cached data samt Middleware til at håndtere cache lifetimes og subscriptions samt selectors og thunks for hvert endpoint (inkl. autogenererede React Hooks der kan kaldes i komponenterne - bemærk når endpointet hedder fetchPopularMovies bliver navnet på det autogenererede hook: useFetchPopularMoviesQuery - det er en useQuery( ) da det er et query og ikke mutation).



Step 4 (store/index.js)

Opret filen index.js:

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/dist/query';
import { moviesApi } from './apis/moviesApi';

export const store = configureStore({
  reducer: {
    [moviesApi.reducerPath]: moviesApi.reducer, //dette er en mere sikker måde, ungår "typo's"
  },
  middleware: (getDefaultMiddleware) => {  //Thunk middelware er default når der benyttes Redux Toolkit configureStore.
    return getDefaultMiddleware()
    .concat(moviesApi.middleware);
  }
});

setupListeners(store.dispatch);

export { useFetchPopularMoviesQuery } from './apis/moviesApi';

Gennemgå og forstå koden.

Hint: Middelware skal både bestå af 'DefaultMiddleware' og den 'Middleware' der genereres via moviesApi

Tjek at der ikke er opstået fejl og at Browseren fortsat viser: "Movies from TheMovieDB"

 

Step 5 (components/movieCard.js)
Opret mappen components med filen movieCard.js:

function MovieCard({movie}){
    const posterBasePath = 'https://image.tmdb.org/t/p/w185_and_h278_bestv2';
    return (
        <div className="col-lg-2 mb-4">
            <div className="card">
                <img src= {posterBasePath + movie.poster_path} className="card-img-top" alt="..."/>
                <div className="card-body">
                    <h5 className="card-title "><span>{movie.title.substring(0,200)}</span></h5><span className="far fa-star" aria-hidden="true"></span><span className="ml-1">{movie.vote_average}</span>
                    <p className="card-text">{movie.overview.substring(0,125).concat('....')}</p>
                    <div className="d-flex justify-content-between p-0"><span className="far fa-calendar" aria-hidden="true"> {movie.release_date}</span><span className="far fa-play-circle"></span></div>            
                </div>
            </div>
        </div>
      );
}

export default MovieCard;

Denne komponent skal vise den enkelte movie inkl. poster, title, vote_average, overview og relese_date. Der benyttes Bootstrap Cards til styling (husk at tilføje Bootstrap cdn samt link til fontawesome i index filen)

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.2.1/slate/bootstrap.min.css" >
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">

<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.slim.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>

 

Step 6 (components/popularMoviesList.js)
Opret en ny fil popularMoviesList.js:

import { useFetchPopularMoviesQuery } from "../store";
import MovieCard from "./movieCard"

function PopularMoviesList() {                                   //Bemærk Query-function kaldes automatisk når komponenten bliver displayed
  const {data, error, isFetching } = useFetchPopularMovies();    //kaldet vil straks hente data i et result-objekt, som vi "destructure" til data, error og isLoading
                                                                 //Bemærk Mutation-function returnere et array med en function, som kan kaldes når data skal ændres
  //console.log(data, error, isFetching);                        //samt et objekt results der er meget tilsvarende det der returneres fra et Query-function kald
                                                                 //til start er results objektet "uinitialiseret", efter kaldet af funktionen vil det indeholde mange flere properties
                                                                 //med relevante værdier fx data, isSucces/isError mm
let content;
  if (isFetching) {
    content = <div>Loading;</div>
  } else if (error) {
    content = <div>Error loading movies.</div>;
  } else {
    content = data.results.map((movie) => {
      return <MovieCard key={movie.id} movie={movie}></MovieCard>
    });
  }
    return (
    <div className="row row-cols-3 row-cols-md-2 m-4">
      {content}
    </div>
  );
}
export default PopularMoviesList;

 

Step 7 (opdatering af index.js)
Husk: Provider skal importeres i index.js:

import { Provider } from 'react-redux';


Husk at wrappe <App/> ind i et <Provider> tag:

  <Provider store={store}>
    <App />
  </Provider>

 

Step 8 (App.js)
Indsæt et tag med vores nye komponent og verificer at du får vist de 20 mest populære film hentet fra TheMovieDB:



Nu er der "hul" igennem til TheMovieDB og vi kan få vist de 20 mest populære film lige nu. Næste skridt er at få lavet nye endpoints, fx til at hente 'highest-ratede' eller 'upcomming' film (se https://developers.themoviedb.org/3/discover/movie-discover og https://developers.themoviedb.org/3/movies/get-upcoming )


Step 9 (store/api/moviesApi.js)

Opret nye endpoints i moviesApi.js, fx:

fetchHighestRatedMovies: builder.query({
        query: () => {
          return {
            url: 'discover/movie',
            params: {
              sort_by: 'vote_average.desc',
              api_key: '81c50c197b83129dd4fc387ca6c8c323'
            },
            method: 'GET',
          };
        },
      }),  

Ps husk at eksportere: useFetchHighestRatedMoviesQuery

 

Step 10 (components/highestRatedMoviesList.js)
Opret en ny fil highestRatedMoviesList.js med en function-component ala PopularMoviesList.

Hint: da flere af de film der returneres ikke indeholder poster og har en rate (vote_average) på 0, ønsker vi at filtrere dem fra:

.filter(movie => movie.poster_path !== null && movie.vote_average !== 0)


 

Step 11 (App.js)
Indsæt et tag med vores nye komponent og verificer at du får vist de (17) film med højeste ratings hentet fra TheMovieDB:


 

I stedet for at få vist begge komponenter samtidigt, skal der tilføjes routing i form af en navigationsbar med link til de to "sider" (komponenter).


Step 12 (installering af react-router)

Først installeres node pakken med react router, kør: 'npm install --save react-router-dom'.

Step 13 (Opdater App.js med installering af react-router)
Opdater App.js med en nav-bar med link til home, popular og highest Rated ala:

import { Routes, Route, Link } from 'react-router-dom';
import HighestRatedMovieList from "./components/highestRatedMoviesList";
import PopularMoviesList from "./components/popularMoviesList";
import MovieImg from './assets/Image/movie_black2.jpg';
import Home from './components/home';

function App() {
  return (
    <div>
      <div className="jumbotron pb-3 pt-3">
        <div className="navbar navbar-expand-lg">
          <nav className="nav navbar-nav">    
            <Link to='/' className="nav-item nav-link">Home</Link>
            <Link to='/popular' className="nav-item nav-link">Popular</Link>
            <Link to='/highest-rated' className="nav-item nav-link">Highest Rated</Link>
          </nav>
        </div>
          <span className='h1'>React Moviefinder <img className="rounded movie_img m-3" src={MovieImg} width="75" height="75"/></span>
      <span className="d-flex justify-content-between p-0">This small App demonstrates React, Redux-Toolkit, RTK Query and React-Router</span>
        </div>
        <Routes>
            <Route path='/' element={<Home/>} />  
            <Route path='/popular' element={<PopularMoviesList/>} />    
            <Route path='/highest-rated' element={<HighestRatedMovieList/>} />
        </Routes>
    </div>
  );
}

 

Step 14 (assets/Image/movie_black2.jpg)
Tilføj mapperne assets/Image under src og gem følgende image i mappen:


 

Step 15 (home.js)
Lav en ny komponent til "siden" home ala:




Step 16 (opdatering af index.js)

Husk: BrowserRouter skal importeres i index.js:

import { BrowserRouter } from 'react-router-dom'

og <App /> skal wrappes ind i et <BrowserRouter> tag:

 <BrowserRouter>
  <Provider store={store}>
    <App />
  </Provider>
 </BrowserRouter>


Step 17 (Ekstra - searchMovie)
Det skal være muligt at søge efter film på TheMovieDB - prøv om du kan implementere en løsning!

Hint 1: Der skal bruges et nyt endpoint ala:

fetchSearchMovie: builder.query({
        query: (searchTerm) => {
          return {
            url: 'search/movie',
            params: {
              query: searchTerm,
              api_key: '81c50c197b83129dd4fc387ca6c8c323'
            },
            method: 'GET',
          };
        },
      }),    

 

Hint 2: Der skal bruges en ny searchMovieSlice, der har en initialState: {searchTerm:' '}, se iøvrigt: ReactReduxToolkitExercise3.html

Hint 3: Der skal laves en komponent searchMovie.js ala:

import { useSelector, useDispatch } from "react-redux";
import { changeSearchTerm } from "../store";
import { useNavigate } from "react-router-dom";

function SearchMovie() {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const searchTerm = useSelector((state) => {
    return state.searchMovie.searchTerm;
  });
  const handleSearchTermChange = (event) => {
    console.log(event.target.value);
    dispatch(changeSearchTerm(event.target.value));
  }
  const handleSubmit = (event) => {
                 //dette for at undgå at Browseren automatisk prøver et udføre et submit  
                 //dispatch(changeSearchTerm(searchTerm));
                 event.preventDefault();
                 navigate("/searchedMovie");
  }
  return (
   <form onSubmit={handleSubmit}>
     <label >Search</label>
     <input className="input ml-2" value={searchTerm} onChange={handleSearchTermChange}/>
     </form>    
  );
}
export default SearchMovie;

Bemærk: der er lavet en programmeringsmæssig navigation med useNavigate( ), hvor der navigeres til en "side" der viser "resultatet" af søgningen (searchedMovieList.js)

 

Slut resultatet kan være noget ala:



 

Step 18 (Ekstra - upcommingMovie)
Implementer en visning af Upcomming film

Step 19 (Ekstra - playMovie)
Implementer mulighed for at afspille trailer til filmen (ved klik på play-icon)

Step 20 (Ekstra - favoriteMovie)
Implementer mulighed for at gemme (ved klik på star-icon) en fil som favorit, samt at få vist dine favorit film på en "side" - det bør også være muligt at fjerne en film fra "listen"

Hint: Benyt JSON-Server til at gemme (persistere) favorite movies: https://www.npmjs.com/package/json-server se også: Media-ReduxToolkitQuery.zip

Bemærk:
For at afprøve app'en skal der oprettes et ekstra terminal-window (split)
I det ene startes json-severen med: 'npm run start:server' og i det andet vindue startes selve app'en med: 'npm start'


Step 21 (Ekstra - TV)
Vis informationer om TV-serier

Step 22 (Ekstra - Genre og Director mm)
Implementer søgning på genre og instruktører mm

Bemærk - kan ved løsning af enkelte ekstra opgaver, benyttes som besvarelse på den obligatoriske opgave!


Congratulation! - RTK Query Jubiiii!

/ Henrik H