diff --git a/samples/react/MusicStore/ReactApp/components/NavMenu.tsx b/samples/react/MusicStore/ReactApp/components/NavMenu.tsx index 55f0fbb..867fc90 100644 --- a/samples/react/MusicStore/ReactApp/components/NavMenu.tsx +++ b/samples/react/MusicStore/ReactApp/components/NavMenu.tsx @@ -12,7 +12,7 @@ class NavMenu extends React.Component { } public render() { - var genres = this.props.genres.slice(0, 5); + const genres = this.props.genres.slice(0, 5); return ( diff --git a/samples/react/MusicStore/ReactApp/components/public/AlbumDetails.tsx b/samples/react/MusicStore/ReactApp/components/public/AlbumDetails.tsx index 1ea14e8..5cac2c7 100644 --- a/samples/react/MusicStore/ReactApp/components/public/AlbumDetails.tsx +++ b/samples/react/MusicStore/ReactApp/components/public/AlbumDetails.tsx @@ -5,22 +5,20 @@ import { ApplicationState } from '../../store'; import * as AlbumDetailsState from '../../store/AlbumDetails'; interface RouteParams { - albumId: number; + albumId: string; } class AlbumDetails extends React.Component { componentWillMount() { - this.props.requestAlbumDetails(this.props.params.albumId); + this.props.requestAlbumDetails(parseInt(this.props.params.albumId)); } componentWillReceiveProps(nextProps: AlbumDetailsProps) { - if (nextProps.params.albumId !== this.props.params.albumId) { - nextProps.requestAlbumDetails(nextProps.params.albumId); - } + this.props.requestAlbumDetails(parseInt(nextProps.params.albumId)); } public render() { - if (this.props.isLoaded) { + if (this.props.album) { const albumData = this.props.album; return

{ albumData.Title }

diff --git a/samples/react/MusicStore/ReactApp/components/public/GenreDetails.tsx b/samples/react/MusicStore/ReactApp/components/public/GenreDetails.tsx index 20a68e7..3ba789f 100644 --- a/samples/react/MusicStore/ReactApp/components/public/GenreDetails.tsx +++ b/samples/react/MusicStore/ReactApp/components/public/GenreDetails.tsx @@ -6,28 +6,25 @@ import * as GenreDetailsStore from '../../store/GenreDetails'; import { AlbumTile } from './AlbumTile'; interface RouteParams { - genreId: number + genreId: string } class GenreDetails extends React.Component { componentWillMount() { - this.props.requestGenreDetails(this.props.params.genreId); + this.props.requestGenreDetails(parseInt(this.props.params.genreId)); } componentWillReceiveProps(nextProps: GenreDetailsProps) { - if (nextProps.params.genreId !== this.props.params.genreId) { - nextProps.requestGenreDetails(nextProps.params.genreId); - } + this.props.requestGenreDetails(parseInt(nextProps.params.genreId)); } public render() { if (this.props.isLoaded) { - let albums = this.props.albums; return

Albums

    - {albums.map(album => + {this.props.albums.map(album => )}
diff --git a/samples/react/MusicStore/ReactApp/components/public/Genres.tsx b/samples/react/MusicStore/ReactApp/components/public/Genres.tsx index 84b8b34..1e03789 100644 --- a/samples/react/MusicStore/ReactApp/components/public/Genres.tsx +++ b/samples/react/MusicStore/ReactApp/components/public/Genres.tsx @@ -6,18 +6,15 @@ import * as GenreList from '../../store/GenreList'; class Genres extends React.Component { componentWillMount() { - if (!this.props.genres.length) { - this.props.requestGenresList(); - } + this.props.requestGenresList(); } public render() { - let { genres } = this.props; - + const { genres } = this.props; return

Browse Genres

-

Select from { genres.length || '...' } genres:

+

Select from { this.props.isLoaded ? genres.length : '...' } genres:

    {genres.map(genre => diff --git a/samples/react/MusicStore/ReactApp/components/public/Home.tsx b/samples/react/MusicStore/ReactApp/components/public/Home.tsx index e82ab96..b0f03d8 100644 --- a/samples/react/MusicStore/ReactApp/components/public/Home.tsx +++ b/samples/react/MusicStore/ReactApp/components/public/Home.tsx @@ -7,20 +7,17 @@ import { AlbumTile } from './AlbumTile'; class Home extends React.Component { componentWillMount() { - if (!this.props.albums.length) { - this.props.requestFeaturedAlbums(); - } + this.props.requestFeaturedAlbums(); } public render() { - let { albums } = this.props; return

    MVC Music Store

      - {albums.map(album => + {this.props.albums.map(album => )}
    diff --git a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts index ee63871..0edb2a4 100644 --- a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts @@ -7,12 +7,12 @@ import { Genre } from './GenreList'; // STATE - This defines the type of data maintained in the Redux store. export interface AlbumDetailsState { - isLoaded: boolean; album: AlbumDetails; + requestedAlbumId: number; } export interface AlbumDetails { - AlbumId: string; + AlbumId: number; Title: string; AlbumArtUrl: string; Genre: Genre; @@ -49,23 +49,31 @@ class ReceiveAlbumDetails extends Action { export const actionCreators = { requestAlbumDetails: (albumId: number): ActionCreator => (dispatch, getState) => { - fetch(`/api/albums/${ albumId }`) - .then(results => results.json()) - .then(album => dispatch(new ReceiveAlbumDetails(album))); - - dispatch(new RequestAlbumDetails(albumId)); + // Only load if it's not already loaded (or currently being loaded) + if (albumId !== getState().albumDetails.requestedAlbumId) { + fetch(`/api/albums/${ albumId }`) + .then(results => results.json()) + .then(album => { + // Only replace state if it's still the most recent request + if (albumId === getState().albumDetails.requestedAlbumId) { + dispatch(new ReceiveAlbumDetails(album)); + } + }); + + dispatch(new RequestAlbumDetails(albumId)); + } } }; // ---------------- // REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state. // For unrecognized actions, must return the existing state (or default initial state if none was supplied). -const unloadedState: AlbumDetailsState = { isLoaded: false, album: null }; +const unloadedState: AlbumDetailsState = { requestedAlbumId: null as number, album: null }; export const reducer: Reducer = (state, action) => { if (isActionType(action, RequestAlbumDetails)) { - return unloadedState; + return { requestedAlbumId: action.albumId, album: null }; } else if (isActionType(action, ReceiveAlbumDetails)) { - return { isLoaded: true, album: action.album }; + return { requestedAlbumId: action.album.AlbumId, album: action.album }; } else { return state || unloadedState; } diff --git a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts index 9d46eda..1824ab2 100644 --- a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts +++ b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts @@ -7,6 +7,7 @@ import { ActionCreator } from './'; export interface FeaturedAlbumsState { albums: Album[]; + isLoaded: boolean; } export interface Album { @@ -37,11 +38,13 @@ class ReceiveFeaturedAlbums extends Action { export const actionCreators = { requestFeaturedAlbums: (): ActionCreator => (dispatch, getState) => { - fetch('/api/albums/mostPopular') - .then(results => results.json()) - .then(albums => dispatch(new ReceiveFeaturedAlbums(albums))); - - return dispatch(new RequestFeaturedAlbums()); + if (!getState().featuredAlbums.isLoaded) { + fetch('/api/albums/mostPopular') + .then(results => results.json()) + .then(albums => dispatch(new ReceiveFeaturedAlbums(albums))); + + return dispatch(new RequestFeaturedAlbums()); + } } }; @@ -51,8 +54,8 @@ export const actionCreators = { export const reducer: Reducer = (state, action) => { if (isActionType(action, ReceiveFeaturedAlbums)) { - return { albums: action.albums }; + return { albums: action.albums, isLoaded: true }; } else { - return state || { albums: [] }; + return state || { albums: [], isLoaded: false }; } }; diff --git a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts index 3db5231..6640e06 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts @@ -7,8 +7,9 @@ import { Album } from './FeaturedAlbums'; // STATE - This defines the type of data maintained in the Redux store. export interface GenreDetailsState { - isLoaded: boolean; + requestedGenreId: number; albums: Album[]; + isLoaded: boolean; } // ----------------- @@ -25,7 +26,7 @@ class RequestGenreDetails extends Action { @typeName("RECEIVE_GENRE_DETAILS") class ReceiveGenreDetails extends Action { - constructor(public albums: Album[]) { + constructor(public genreId: number, public albums: Album[]) { super(); } } @@ -36,24 +37,31 @@ class ReceiveGenreDetails extends Action { export const actionCreators = { requestGenreDetails: (genreId: number): ActionCreator => (dispatch, getState) => { - fetch(`/api/genres/${ genreId }/albums`) - .then(results => results.json()) - .then(albums => dispatch(new ReceiveGenreDetails(albums))); - - dispatch(new RequestGenreDetails(genreId)); + // Only load if it's not already loaded (or currently being loaded) + if (genreId !== getState().genreDetails.requestedGenreId) { + fetch(`/api/genres/${ genreId }/albums`) + .then(results => results.json()) + .then(albums => { + // Only replace state if it's still the most recent request + if (genreId === getState().genreDetails.requestedGenreId) { + dispatch(new ReceiveGenreDetails(genreId, albums)); + } + }); + + dispatch(new RequestGenreDetails(genreId)); + } } }; // ---------------- // REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state. // For unrecognized actions, must return the existing state (or default initial state if none was supplied). -const unloadedState: GenreDetailsState = { isLoaded: false, albums: [] }; export const reducer: Reducer = (state, action) => { if (isActionType(action, RequestGenreDetails)) { - return unloadedState; + return { requestedGenreId: action.genreId, albums: [], isLoaded: false }; } else if (isActionType(action, ReceiveGenreDetails)) { - return { isLoaded: true, albums: action.albums }; + return { requestedGenreId: action.genreId, albums: action.albums, isLoaded: true }; } else { - return state || unloadedState; + return state || { requestedGenreId: null as number, albums: [], isLoaded: false }; } }; diff --git a/samples/react/MusicStore/ReactApp/store/GenreList.ts b/samples/react/MusicStore/ReactApp/store/GenreList.ts index fbddbcf..133fad4 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreList.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreList.ts @@ -7,6 +7,7 @@ import { ActionCreator } from './'; export interface GenresListState { genres: Genre[]; + isLoaded: boolean; } export interface Genre { @@ -32,9 +33,11 @@ class ReceiveGenresList extends Action { export const actionCreators = { requestGenresList: (): ActionCreator => (dispatch, getState) => { - fetch('/api/genres') - .then(results => results.json()) - .then(genres => dispatch(new ReceiveGenresList(genres))); + if (!getState().genreList.isLoaded) { + fetch('/api/genres') + .then(results => results.json()) + .then(genres => dispatch(new ReceiveGenresList(genres))); + } } }; @@ -44,8 +47,8 @@ export const actionCreators = { export const reducer: Reducer = (state, action) => { if (isActionType(action, ReceiveGenresList)) { - return { genres: action.genres }; + return { genres: action.genres, isLoaded: true }; } else { - return state || { genres: [] }; + return state || { genres: [], isLoaded: false }; } };