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() { public render() {
var genres = this.props.genres.slice(0, 5); const genres = this.props.genres.slice(0, 5);
return ( return (
<Navbar inverse fixedTop> <Navbar inverse fixedTop>
<Navbar.Header> <Navbar.Header>

View File

@@ -5,22 +5,20 @@ import { ApplicationState } from '../../store';
import * as AlbumDetailsState from '../../store/AlbumDetails'; import * as AlbumDetailsState from '../../store/AlbumDetails';
interface RouteParams { interface RouteParams {
albumId: number; albumId: string;
} }
class AlbumDetails extends React.Component<AlbumDetailsProps, void> { class AlbumDetails extends React.Component<AlbumDetailsProps, void> {
componentWillMount() { componentWillMount() {
this.props.requestAlbumDetails(this.props.params.albumId); this.props.requestAlbumDetails(parseInt(this.props.params.albumId));
} }
componentWillReceiveProps(nextProps: AlbumDetailsProps) { componentWillReceiveProps(nextProps: AlbumDetailsProps) {
if (nextProps.params.albumId !== this.props.params.albumId) { this.props.requestAlbumDetails(parseInt(nextProps.params.albumId));
nextProps.requestAlbumDetails(nextProps.params.albumId);
}
} }
public render() { public render() {
if (this.props.isLoaded) { if (this.props.album) {
const albumData = this.props.album; const albumData = this.props.album;
return <div> return <div>
<h2>{ albumData.Title }</h2> <h2>{ albumData.Title }</h2>

View File

@@ -6,28 +6,25 @@ import * as GenreDetailsStore from '../../store/GenreDetails';
import { AlbumTile } from './AlbumTile'; import { AlbumTile } from './AlbumTile';
interface RouteParams { interface RouteParams {
genreId: number genreId: string
} }
class GenreDetails extends React.Component<GenreDetailsProps, void> { class GenreDetails extends React.Component<GenreDetailsProps, void> {
componentWillMount() { componentWillMount() {
this.props.requestGenreDetails(this.props.params.genreId); this.props.requestGenreDetails(parseInt(this.props.params.genreId));
} }
componentWillReceiveProps(nextProps: GenreDetailsProps) { componentWillReceiveProps(nextProps: GenreDetailsProps) {
if (nextProps.params.genreId !== this.props.params.genreId) { this.props.requestGenreDetails(parseInt(nextProps.params.genreId));
nextProps.requestGenreDetails(nextProps.params.genreId);
}
} }
public render() { public render() {
if (this.props.isLoaded) { if (this.props.isLoaded) {
let albums = this.props.albums;
return <div> return <div>
<h3>Albums</h3> <h3>Albums</h3>
<ul className="list-unstyled"> <ul className="list-unstyled">
{albums.map(album => {this.props.albums.map(album =>
<AlbumTile key={ album.AlbumId } album={ album } /> <AlbumTile key={ album.AlbumId } album={ album } />
)} )}
</ul> </ul>

View File

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

View File

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

View File

@@ -7,12 +7,12 @@ import { Genre } from './GenreList';
// STATE - This defines the type of data maintained in the Redux store. // STATE - This defines the type of data maintained in the Redux store.
export interface AlbumDetailsState { export interface AlbumDetailsState {
isLoaded: boolean;
album: AlbumDetails; album: AlbumDetails;
requestedAlbumId: number;
} }
export interface AlbumDetails { export interface AlbumDetails {
AlbumId: string; AlbumId: number;
Title: string; Title: string;
AlbumArtUrl: string; AlbumArtUrl: string;
Genre: Genre; Genre: Genre;
@@ -49,23 +49,31 @@ class ReceiveAlbumDetails extends Action {
export const actionCreators = { export const actionCreators = {
requestAlbumDetails: (albumId: number): ActionCreator => (dispatch, getState) => { requestAlbumDetails: (albumId: number): ActionCreator => (dispatch, getState) => {
fetch(`/api/albums/${ albumId }`) // Only load if it's not already loaded (or currently being loaded)
.then(results => results.json()) if (albumId !== getState().albumDetails.requestedAlbumId) {
.then(album => dispatch(new ReceiveAlbumDetails(album))); 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)); 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. // 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). // 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) => { export const reducer: Reducer<AlbumDetailsState> = (state, action) => {
if (isActionType(action, RequestAlbumDetails)) { if (isActionType(action, RequestAlbumDetails)) {
return unloadedState; return { requestedAlbumId: action.albumId, album: null };
} else if (isActionType(action, ReceiveAlbumDetails)) { } else if (isActionType(action, ReceiveAlbumDetails)) {
return { isLoaded: true, album: action.album }; return { requestedAlbumId: action.album.AlbumId, album: action.album };
} else { } else {
return state || unloadedState; return state || unloadedState;
} }

View File

@@ -7,6 +7,7 @@ import { ActionCreator } from './';
export interface FeaturedAlbumsState { export interface FeaturedAlbumsState {
albums: Album[]; albums: Album[];
isLoaded: boolean;
} }
export interface Album { export interface Album {
@@ -37,11 +38,13 @@ class ReceiveFeaturedAlbums extends Action {
export const actionCreators = { export const actionCreators = {
requestFeaturedAlbums: (): ActionCreator => (dispatch, getState) => { requestFeaturedAlbums: (): ActionCreator => (dispatch, getState) => {
fetch('/api/albums/mostPopular') if (!getState().featuredAlbums.isLoaded) {
.then(results => results.json()) fetch('/api/albums/mostPopular')
.then(albums => dispatch(new ReceiveFeaturedAlbums(albums))); .then(results => results.json())
.then(albums => dispatch(new ReceiveFeaturedAlbums(albums)));
return dispatch(new RequestFeaturedAlbums()); return dispatch(new RequestFeaturedAlbums());
}
} }
}; };
@@ -51,8 +54,8 @@ export const actionCreators = {
export const reducer: Reducer<FeaturedAlbumsState> = (state, action) => { export const reducer: Reducer<FeaturedAlbumsState> = (state, action) => {
if (isActionType(action, ReceiveFeaturedAlbums)) { if (isActionType(action, ReceiveFeaturedAlbums)) {
return { albums: action.albums }; return { albums: action.albums, isLoaded: true };
} else { } 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. // STATE - This defines the type of data maintained in the Redux store.
export interface GenreDetailsState { export interface GenreDetailsState {
isLoaded: boolean; requestedGenreId: number;
albums: Album[]; albums: Album[];
isLoaded: boolean;
} }
// ----------------- // -----------------
@@ -25,7 +26,7 @@ class RequestGenreDetails extends Action {
@typeName("RECEIVE_GENRE_DETAILS") @typeName("RECEIVE_GENRE_DETAILS")
class ReceiveGenreDetails extends Action { class ReceiveGenreDetails extends Action {
constructor(public albums: Album[]) { constructor(public genreId: number, public albums: Album[]) {
super(); super();
} }
} }
@@ -36,24 +37,31 @@ class ReceiveGenreDetails extends Action {
export const actionCreators = { export const actionCreators = {
requestGenreDetails: (genreId: number): ActionCreator => (dispatch, getState) => { requestGenreDetails: (genreId: number): ActionCreator => (dispatch, getState) => {
fetch(`/api/genres/${ genreId }/albums`) // Only load if it's not already loaded (or currently being loaded)
.then(results => results.json()) if (genreId !== getState().genreDetails.requestedGenreId) {
.then(albums => dispatch(new ReceiveGenreDetails(albums))); 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)); 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. // 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). // 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) => { export const reducer: Reducer<GenreDetailsState> = (state, action) => {
if (isActionType(action, RequestGenreDetails)) { if (isActionType(action, RequestGenreDetails)) {
return unloadedState; return { requestedGenreId: action.genreId, albums: [], isLoaded: false };
} else if (isActionType(action, ReceiveGenreDetails)) { } else if (isActionType(action, ReceiveGenreDetails)) {
return { isLoaded: true, albums: action.albums }; return { requestedGenreId: action.genreId, albums: action.albums, isLoaded: true };
} else { } 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 { export interface GenresListState {
genres: Genre[]; genres: Genre[];
isLoaded: boolean;
} }
export interface Genre { export interface Genre {
@@ -32,9 +33,11 @@ class ReceiveGenresList extends Action {
export const actionCreators = { export const actionCreators = {
requestGenresList: (): ActionCreator => (dispatch, getState) => { requestGenresList: (): ActionCreator => (dispatch, getState) => {
fetch('/api/genres') if (!getState().genreList.isLoaded) {
.then(results => results.json()) fetch('/api/genres')
.then(genres => dispatch(new ReceiveGenresList(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) => { export const reducer: Reducer<GenresListState> = (state, action) => {
if (isActionType(action, ReceiveGenresList)) { if (isActionType(action, ReceiveGenresList)) {
return { genres: action.genres }; return { genres: action.genres, isLoaded: true };
} else { } else {
return state || { genres: [] }; return state || { genres: [], isLoaded: false };
} }
}; };