Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HT-5: fetch data asynchronously #71

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/components/menu/menu.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
import Product from '../product';
import Basket from '../basket';
import Loader from '../loader';
import { loadProducts } from '../../redux/actions';
import {
isRestaurantProductsLoading as isLoading,
isRestaurantProductsLoaded as isLoaded
} from '../../redux/selectors';

import styles from './menu.module.css';

Expand All @@ -16,8 +24,20 @@ class Menu extends React.Component {
this.setState({ error });
}

componentDidMount() {
this.loadProducts();
}

componentDidUpdate() {
this.loadProducts();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут еще нужно проверить поменялся ли restaurantId, чтобы логика из loadProducts вызывалась только при смене ресторана

Copy link
Author

@vskosp vskosp Dec 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

она не будет вызываться для уже загруженных ресторанов из-за этой проверки (isLoading и isLoaded будут передаваться для текущего ресторана) https://github.com/koretskiyav/react-2020-11-13/pull/71/files#diff-4d8fc5329143e8a6d6c5486e865a6ac6ad20a19f415fbf280fab745160f398aeR62

loadProducts() {
    const { isLoading, isLoaded, loadProducts } = this.props;
    if (!isLoading && !isLoaded) loadProducts();
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

сейчас да, но код часто меняется и это менее очевидно, чем вызывать это только при смене ресторана. То есть если я зайду в этот код, через 3 месяца, я хочу понять что тут происходит и какая логика заложена. Я хочу в коде видеть, буквально "если у нас поменялся ресторан идем грузить продукты".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

явное всегда лучше неявного

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

согласен

}

render() {
const { menu } = this.props;
const { menu, isLoading, isLoaded } = this.props;

if (isLoading || !isLoaded) {
return <Loader />
}

if (this.state.error) {
return <p>В этом ресторане меню не доступно</p>;
Expand All @@ -36,6 +56,20 @@ class Menu extends React.Component {
</div>
);
}

loadProducts() {
const { isLoading, isLoaded, loadProducts } = this.props;
if (!isLoading && !isLoaded) loadProducts();
}
}

export default Menu;
const mapStateToProps = createStructuredSelector({
isLoading,
isLoaded
});

const mapDispatchToProps = (dispatch, ownProps) => ({
loadProducts: () => dispatch(loadProducts(ownProps.restaurantId))
});

export default connect(mapStateToProps, mapDispatchToProps)(Menu);
35 changes: 27 additions & 8 deletions src/components/restaurant/restaurant.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import React from 'react';
import React, {useEffect} from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
import Menu from '../menu';
import Reviews from '../reviews';
import Banner from '../banner';
import Rate from '../rate';
import Tabs from '../tabs';
import { averageRatingSelector } from '../../redux/selectors';
import Loader from '../loader';
import { loadReviews } from '../../redux/actions';
import {
averageRatingSelector as averageRating,
isRestaurantReviewsLoading as isReviewsLoading,
isRestaurantReviewsLoaded as isReviewsLoaded
} from '../../redux/selectors';

const Restaurant = ({ id, name, menu, reviews, averageRating, loadReviews, isReviewsLoading, isReviewsLoaded }) => {
useEffect(() => {
if (!isReviewsLoading && !isReviewsLoaded) loadReviews(id);
}, [isReviewsLoading, isReviewsLoaded, loadReviews, id]);

const Restaurant = ({ id, name, menu, reviews, averageRating }) => {
const tabs = [
{ title: 'Menu', content: <Menu menu={menu} /> },
{ title: 'Menu', content: <Menu menu={menu} restaurantId={id} /> },
{
title: 'Reviews',
content: <Reviews reviews={reviews} restaurantId={id} />,
Expand All @@ -20,7 +31,11 @@ const Restaurant = ({ id, name, menu, reviews, averageRating }) => {
return (
<div>
<Banner heading={name}>
<Rate value={averageRating} />
{
isReviewsLoading || !isReviewsLoaded
? <Loader />
: <Rate value={averageRating} />
}
</Banner>
<Tabs tabs={tabs} />
</div>
Expand All @@ -35,6 +50,10 @@ Restaurant.propTypes = {
averageRating: PropTypes.number,
};

export default connect((state, props) => ({
averageRating: averageRatingSelector(state, props),
}))(Restaurant);
const mapStateToProps = createStructuredSelector({
isReviewsLoading,
isReviewsLoaded,
averageRating
});

export default connect(mapStateToProps, { loadReviews })(Restaurant);
24 changes: 18 additions & 6 deletions src/components/reviews/reviews.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import React, { useEffect } from 'react';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
import Loader from '../loader';
import Review from './review';
import ReviewForm from './review-form';
import styles from './reviews.module.css';
import { connect } from 'react-redux';
import { loadUsers } from '../../redux/actions';
import {
isRestaurantReviewsLoading as isLoading,
isRestaurantReviewsLoaded as isLoaded
} from '../../redux/selectors';

import { loadReviews } from '../../redux/actions';
const Reviews = ({ reviews, restaurantId, loadUsers, isLoading, isLoaded }) => {
useEffect(() => loadUsers(), [loadUsers]);

const Reviews = ({ reviews, restaurantId, loadReviews }) => {
useEffect(() => {
loadReviews(restaurantId);
}, [loadReviews, restaurantId]);
if (isLoading || !isLoaded) {
return <Loader />
}

return (
<div className={styles.reviews}>
Expand All @@ -27,4 +34,9 @@ Reviews.propTypes = {
reviews: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
};

export default connect(null, { loadReviews })(Reviews);
const mapStateToProps = createStructuredSelector({
isLoading,
isLoaded
});

export default connect(mapStateToProps, { loadUsers })(Reviews);
25 changes: 22 additions & 3 deletions src/redux/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
REMOVE,
ADD_REVIEW,
LOAD_RESTAURANTS,
LOAD_PRODUCTS,
LOAD_REVIEWS,
LOAD_USERS,
REQUEST,
SUCCESS,
FAILURE,
Expand All @@ -25,14 +27,31 @@ export const loadRestaurants = () => ({
CallAPI: '/api/restaurants',
});

export const loadProducts = (restaurantId) => ({
type: LOAD_PRODUCTS,
CallAPI: `/api/products?id=${restaurantId}`,
params: { id: restaurantId }
});

export const loadReviews = (restaurantId) => async (dispatch) => {
dispatch({ type: LOAD_REVIEWS + REQUEST });
const params = { id: restaurantId };
dispatch({ type: LOAD_REVIEWS + REQUEST, params });
try {
const response = await fetch(
`/api/reviews?id=${restaurantId}`
).then((res) => res.json());
dispatch({ type: LOAD_REVIEWS + SUCCESS, response });
dispatch({ type: LOAD_REVIEWS + SUCCESS, params, response });
} catch (error) {
dispatch({ type: LOAD_REVIEWS + FAILURE, params, error });
}
};

export const loadUsers = () => async (dispatch) => {
dispatch({ type: LOAD_USERS + REQUEST });
try {
const response = await fetch('/api/users').then((res) => res.json());
dispatch({ type: LOAD_USERS + SUCCESS, response });
} catch (error) {
dispatch({ type: LOAD_REVIEWS + FAILURE, error });
dispatch({ type: LOAD_USERS + FAILURE, error });
}
};
2 changes: 2 additions & 0 deletions src/redux/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export const REMOVE = 'REMOVE';
export const ADD_REVIEW = 'ADD_REVIEW';

export const LOAD_RESTAURANTS = 'LOAD_RESTAURANTS';
export const LOAD_PRODUCTS = 'LOAD_PRODUCTS';
export const LOAD_REVIEWS = 'LOAD_REVIEWS';
export const LOAD_USERS = 'LOAD_USERS';

export const REQUEST = '_REQUEST';
export const SUCCESS = '_SUCCESS';
Expand Down
16 changes: 11 additions & 5 deletions src/redux/reducer/products.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { normalizedProducts } from '../../fixtures';
import { arrToMap } from '../utils';
import { combineReducers } from 'redux';
import { arrToMap, listByIdReducer } from '../utils';
import { LOAD_PRODUCTS, SUCCESS } from '../constants'

// { [productId]: product }
const reducer = (state = arrToMap(normalizedProducts), action) => {
const { type } = action;
const entities = (state = {}, action) => {
const { type, response } = action;

switch (type) {
case LOAD_PRODUCTS + SUCCESS:
return { ...state, ...arrToMap(response) };
default:
return state;
}
};

export default reducer;
export default combineReducers({
entities,
listByRestaurant: listByIdReducer(LOAD_PRODUCTS)
});
17 changes: 11 additions & 6 deletions src/redux/reducer/reviews.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ADD_REVIEW } from '../constants';
import { normalizedReviews } from '../../fixtures';
import { arrToMap } from '../utils';
import { combineReducers } from 'redux';
import { ADD_REVIEW, LOAD_REVIEWS, SUCCESS} from '../constants';
import { arrToMap, listByIdReducer } from '../utils';

const reducer = (state = arrToMap(normalizedReviews), action) => {
const { type, payload, reviewId, userId } = action;
const entities = (state = {}, action) => {
const { type, payload, response, reviewId, userId } = action;

switch (type) {
case ADD_REVIEW:
Expand All @@ -12,9 +12,14 @@ const reducer = (state = arrToMap(normalizedReviews), action) => {
...state,
[reviewId]: { id: reviewId, userId, text, rating },
};
case LOAD_REVIEWS + SUCCESS:
return { ...state, ...arrToMap(response) };
default:
return state;
}
};

export default reducer;
export default combineReducers({
entities,
listByRestaurant: listByIdReducer(LOAD_REVIEWS)
});
10 changes: 6 additions & 4 deletions src/redux/reducer/users.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import produce from 'immer';
import { ADD_REVIEW } from '../constants';
import { normalizedUsers } from '../../fixtures';
import { ADD_REVIEW, LOAD_USERS, SUCCESS } from '../constants';
import { arrToMap } from '../utils';

const reducer = produce((draft = arrToMap(normalizedUsers), action) => {
const { type, payload, userId } = action;
const reducer = produce((draft = {}, action) => {
const { type, payload, userId, response } = action;

switch (type) {
case ADD_REVIEW:
const { name } = payload.review;
draft[userId] = { id: userId, name };
break;
case LOAD_USERS + SUCCESS:
Object.assign(draft, arrToMap(response));
break;
default:
return draft;
}
Expand Down
29 changes: 26 additions & 3 deletions src/redux/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getById } from './utils';

const restaurantsSelector = (state) => state.restaurants.entities;
const orderSelector = (state) => state.order;
const productsSelector = (state) => state.products;
const productsSelector = (state) => state.products.entities;

export const restaurantsLoadingSelector = (state) => state.restaurants.loading;
export const restaurantsLoadedSelector = (state) => state.restaurants.loaded;
Expand All @@ -29,7 +29,7 @@ export const totalSelector = createSelector(
orderProducts.reduce((acc, { subtotal }) => acc + subtotal, 0)
);

const reviewsSelector = (state) => state.reviews;
const reviewsSelector = (state) => state.reviews.entities;
const usersSelector = (state) => state.users;

export const restaurantsListSelector = createSelector(
Expand All @@ -53,9 +53,32 @@ export const averageRatingSelector = createSelector(
reviewsSelector,
(_, { reviews }) => reviews,
(reviews, ids) => {
const ratings = ids.map((id) => reviews[id].rating);
const ratings = ids.map((id) => reviews[id]?.rating);
return Math.round(
ratings.reduce((acc, rating) => acc + rating) / ratings.length
);
}
);

const restaurantProductsSelector = (state, props) => state.products.listByRestaurant[props.restaurantId];
const restaurantReviewsSelector = (state, props) => state.reviews.listByRestaurant[props.restaurantId || props.id];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше пропу переименовать, чтобы везде было, к примеру, props.restaurantId


export const isRestaurantProductsLoading = createSelector(
restaurantProductsSelector,
(restaurantProducts) => restaurantProducts && restaurantProducts.isLoading
);

export const isRestaurantProductsLoaded = createSelector(
restaurantProductsSelector,
(restaurantProducts) => restaurantProducts && restaurantProducts.isLoaded
);

export const isRestaurantReviewsLoading = createSelector(
restaurantReviewsSelector,
(restaurantReviews) => restaurantReviews && restaurantReviews.isLoading
);

export const isRestaurantReviewsLoaded = createSelector(
restaurantReviewsSelector,
(restaurantReviews) => restaurantReviews && restaurantReviews.isLoaded
);
34 changes: 34 additions & 0 deletions src/redux/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import {FAILURE, REQUEST, SUCCESS} from './constants';

export const arrToMap = (arr) =>
arr.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});
Expand All @@ -9,3 +10,36 @@ export const getById = (selector, defaultValue) =>
(_, props) => props.id,
(entity, id) => entity[id] || defaultValue
);

export const listByIdReducer = (actionType) => (state = {}, action) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

прикольное решение

const { type, params } = action;

switch (type) {
case actionType + REQUEST:
return {
...state,
[params.id]: {
isLoading: true,
isLoaded: false
}
};
case actionType + SUCCESS:
return {
...state,
[params.id]: {
isLoading: false,
isLoaded: true
}
};
case actionType + FAILURE:
return {
...state,
[params.id]: {
isLoading: false,
isLoaded: false
}
};
default:
return state;
}
};