Ensure data is only loaded if not already loaded (needed to keep client/server state consistent)

This commit is contained in:
SteveSandersonMS
2016-02-09 23:06:30 -08:00
parent 3a567823e8
commit a84688d65c
9 changed files with 69 additions and 58 deletions

View File

@@ -12,7 +12,7 @@ class NavMenu extends React.Component<NavMenuProps, void> {
}
public render() {
var genres = this.props.genres.slice(0, 5);
const genres = this.props.genres.slice(0, 5);
return (
<Navbar inverse fixedTop>
<Navbar.Header>

View File

@@ -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<AlbumDetailsProps, void> {
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 <div>
<h2>{ albumData.Title }</h2>

View File

@@ -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<GenreDetailsProps, void> {
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 <div>
<h3>Albums</h3>
<ul className="list-unstyled">
{albums.map(album =>
{this.props.albums.map(album =>
<AlbumTile key={ album.AlbumId } album={ album } />
)}
</ul>

View File

@@ -6,18 +6,15 @@ import * as GenreList from '../../store/GenreList';
class Genres extends React.Component<GenresProps, void> {
componentWillMount() {
if (!this.props.genres.length) {
this.props.requestGenresList();
}
this.props.requestGenresList();
}
public render() {
let { genres } = this.props;
const { genres } = this.props;
return <div>
<h3>Browse Genres</h3>
<p>Select from { genres.length || '...' } genres:</p>
<p>Select from { this.props.isLoaded ? genres.length : '...' } genres:</p>
<ul className="list-group">
{genres.map(genre =>

View File

@@ -7,20 +7,17 @@ import { AlbumTile } from './AlbumTile';
class Home extends React.Component<HomeProps, void> {
componentWillMount() {
if (!this.props.albums.length) {
this.props.requestFeaturedAlbums();
}
this.props.requestFeaturedAlbums();
}
public render() {
let { albums } = this.props;
return <div>
<div className="jumbotron">
<h1>MVC Music Store</h1>
<img src="/Images/home-showcase.png" />
</div>
<ul className="row list-unstyled" id="album-list">
{albums.map(album =>
{this.props.albums.map(album =>
<AlbumTile key={ album.AlbumId } album={ album } />
)}
</ul>

View File

@@ -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<AlbumDetailsState> = (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;
}

View File

@@ -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<FeaturedAlbumsState> = (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 };
}
};

View File

@@ -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<GenreDetailsState> = (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 };
}
};

View File

@@ -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<GenresListState> = (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 };
}
};