diff --git a/lectures/04-global-state-management/README.md b/lectures/04-global-state-management/README.md new file mode 100644 index 0000000..3aa7b2d --- /dev/null +++ b/lectures/04-global-state-management/README.md @@ -0,0 +1,72 @@ +# Global state management + +- [Presentation](https://docs.google.com/presentation/d/1YItCqFTK-_SzD8sO_uTrae9tw_naE29pl287QITvu1Y/edit?usp=sharing) +- [Video]() + +## Immutability examples + +### Working with objects + +```js +const user = { + id: 1, + firstName: 'John', + lastName: 'Doe', +} +``` + +**Add property** + +```js +user.age = 30 // wrong +const newUser = { ...user, age: 30 } // right +``` + +**Remove property** + +```js +delete user.age // wrong +const { age, ...newUser } = user // right +``` + +**Update property** + +```js +user.firstName = 'Jane' // wrong +const newUser = { ...user, firstName: 'Jane' } // right +``` + +### Working with arrays + +```js +const users = [] +``` + +**Add item** + +```js +users.push(user) // wrong +const newUsers = [...users, user] // right +``` + +**Remove item** + +```js +const users.pop() // wrong +const newUsers = users.filter(u => u.id !== user.id) // right +``` + +**Update item** + +```js +users[0].firstName = 'Jane' // wrong +const newUsers = users.map(u => { + if (u.id === user.id) { + return { + ...user, + firstName: 'Jane', + } + } + return u +}) // right +``` diff --git a/package.json b/package.json index b5a911b..5dc4753 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "dependencies": { "react": "^16.8.4", "react-dom": "^16.8.4", + "react-redux": "^6.0.1", "react-router-dom": "^5.0.0", "react-scripts": "2.1.8", + "redux": "^4.0.1", "sanitize.css": "^8.0.0", "styled-components": "^4.2.0", "styled-system": "^4.0.8" diff --git a/src/App.js b/src/App.js index e4fbd84..1dfd87f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,21 +1,26 @@ import React, { Component } from 'react' import { Switch, Route } from 'react-router-dom' +import { Provider } from 'react-redux' import GlobalStyles from './globalStyles' - import { ProductList } from './pages/ProductList' import { ProductDetail } from './pages/ProductDetail' +import { Cart } from './pages/Cart' +import store from './store' class App extends Component { render() { return ( - - - - - - - + + + + + + + + + + ) } } diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js index baaa084..2d79e1a 100644 --- a/src/components/Layout/index.js +++ b/src/components/Layout/index.js @@ -1,7 +1,32 @@ +import React, { Component, Fragment } from 'react' +import { Link } from 'react-router-dom' import styled from 'styled-components' -const Layout = styled.div` +const Wrapper = styled.div` padding: 2rem; ` +const Header = styled.header` + padding: 3rem; + border-bottom: 0.1rem solid gainsboro; +` + +const StyledLink = styled(Link)` + margin-right: 1rem; +` + +class Layout extends Component { + render() { + return ( + +
+ All Products + My Cart +
+ {this.props.children} +
+ ) + } +} + export default Layout diff --git a/src/pages/Cart/index.js b/src/pages/Cart/index.js new file mode 100644 index 0000000..4d8c921 --- /dev/null +++ b/src/pages/Cart/index.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import Layout from '../../components/Layout' +import { H1 } from '../../components/Typography' + +class CartView extends Component { + render() { + return ( + +

Your cart

+ +
+ ) + } +} + +const mapStateToProps = state => ({ + items: Object.keys(state.cartItems).map(productId => ({ + quantity: state.cartItems[productId], + product: state.products.find(p => p.id === productId), + })), +}) + +const Cart = connect(mapStateToProps)(CartView) + +export { Cart } diff --git a/src/pages/ProductList/components/ProductList/Product/index.js b/src/pages/ProductList/Product/index.js similarity index 56% rename from src/pages/ProductList/components/ProductList/Product/index.js rename to src/pages/ProductList/Product/index.js index 6a39dba..c258ab0 100644 --- a/src/pages/ProductList/components/ProductList/Product/index.js +++ b/src/pages/ProductList/Product/index.js @@ -1,7 +1,16 @@ import React from 'react' -import { Wrapper, ImgWrap, Img, TitleWrap, Title, Price, Link } from './styled' +import { + Wrapper, + ImgWrap, + Img, + TitleWrap, + Title, + Price, + Link, + AddButton, +} from './styled' -const Product = ({ node }) => ( +const Product = ({ node, onAddToCart }) => ( @@ -11,6 +20,9 @@ const Product = ({ node }) => ( {node.name} {node.price.formatted_amount} + onAddToCart(node.id, evt)}> + Add to Cart + ) diff --git a/src/pages/ProductList/components/ProductList/Product/styled.js b/src/pages/ProductList/Product/styled.js similarity index 78% rename from src/pages/ProductList/components/ProductList/Product/styled.js rename to src/pages/ProductList/Product/styled.js index 93d3d9f..b1b9d54 100644 --- a/src/pages/ProductList/components/ProductList/Product/styled.js +++ b/src/pages/ProductList/Product/styled.js @@ -1,6 +1,6 @@ import styled from 'styled-components/macro' import { Link as BaseLink } from 'react-router-dom' -import theme from '../../../../../common/theme' +import theme from '../../../common/theme' export const Wrapper = styled.li`` @@ -45,3 +45,12 @@ export const Title = styled.h3` font-weight: 100; text-transform: uppercase; ` + +export const AddButton = styled.button` + background: ${theme.color.red}; + padding: 1rem; + margin-top: 0.5rem; + border: none; + border-radius: ${theme.radius.basic}; + color: ${theme.color.white}; +` diff --git a/src/pages/ProductList/components/ProductList/index.js b/src/pages/ProductList/components/ProductList/index.js deleted file mode 100644 index a402fd7..0000000 --- a/src/pages/ProductList/components/ProductList/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import Product from './Product' -import { ProductsWrap } from './styled' - -const ProductList = ({ products }) => ( - - {products.map(product => ( - - ))} - -) - -export default ProductList diff --git a/src/pages/ProductList/index.js b/src/pages/ProductList/index.js index 73d6a68..25a29a5 100644 --- a/src/pages/ProductList/index.js +++ b/src/pages/ProductList/index.js @@ -1,38 +1,68 @@ import React, { Component } from 'react' +import { connect } from 'react-redux' -import ProductListComponent from './components/ProductList' import Layout from '../../components/Layout' import Loader from '../../components/Loader' import { H1 } from '../../components/Typography' import { getProducts } from '../../api/get-products' +import { addProduct } from '../../store/cartItems/actions' +import { loadProducts } from '../../store/products/actions' +import Product from './Product' +import { ProductsWrap } from './styled' -class ProductList extends Component { +class Products extends Component { state = { isLoading: true, - products: [], } async componentDidMount() { - let products = await getProducts() + if (this.props.products.length === 0) { + const products = await getProducts() + this.props.loadProducts(products) + } this.setState({ isLoading: false, - products, }) } - render() { - const { isLoading, products } = this.state + handleAddToCart = (productId, evt) => { + evt.preventDefault() + this.props.addProduct(productId) + } + render() { return (

E-Commerce app

- {isLoading && } - {products && } + {this.state.isLoading && } + + {this.props.products.map(product => ( + + ))} +
) } } +const mapStateToProps = state => ({ + products: state.products, +}) + +const mapDispatchToProps = { + loadProducts, + addProduct, +} + +const ProductList = connect( + mapStateToProps, + mapDispatchToProps +)(Products) + export { ProductList } diff --git a/src/pages/ProductList/components/ProductList/styled.js b/src/pages/ProductList/styled.js similarity index 100% rename from src/pages/ProductList/components/ProductList/styled.js rename to src/pages/ProductList/styled.js diff --git a/src/store/cartItems/actions.js b/src/store/cartItems/actions.js new file mode 100644 index 0000000..0387a03 --- /dev/null +++ b/src/store/cartItems/actions.js @@ -0,0 +1,6 @@ +export const ADD_PRODUCT = 'cartItems/ADD' + +export const addProduct = productId => ({ + type: ADD_PRODUCT, + payload: productId, +}) diff --git a/src/store/cartItems/index.js b/src/store/cartItems/index.js new file mode 100644 index 0000000..0e5d05b --- /dev/null +++ b/src/store/cartItems/index.js @@ -0,0 +1,15 @@ +import { ADD_PRODUCT } from './actions' + +const reducer = (state = {}, action) => { + switch (action.type) { + case ADD_PRODUCT: + return { + ...state, + [action.payload]: (state[action.payload] || 0) + 1, + } + default: + return state + } +} + +export default reducer diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..249a0b9 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,13 @@ +import { createStore, combineReducers } from 'redux' + +import products from './products' +import cartItems from './cartItems' + +const reducer = combineReducers({ + products, + cartItems, +}) + +const store = createStore(reducer) + +export default store diff --git a/src/store/products/actions.js b/src/store/products/actions.js new file mode 100644 index 0000000..dcedf5c --- /dev/null +++ b/src/store/products/actions.js @@ -0,0 +1,6 @@ +export const LOAD_PRODUCTS = 'products/LOAD' + +export const loadProducts = products => ({ + type: LOAD_PRODUCTS, + payload: products, +}) diff --git a/src/store/products/index.js b/src/store/products/index.js new file mode 100644 index 0000000..da6cbad --- /dev/null +++ b/src/store/products/index.js @@ -0,0 +1,12 @@ +import { LOAD_PRODUCTS } from './actions' + +const reducer = (state = [], action) => { + switch (action.type) { + case LOAD_PRODUCTS: + return action.payload + default: + return state + } +} + +export default reducer diff --git a/yarn.lock b/yarn.lock index cfe6ef1..d6ae179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,7 +780,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2": version "7.4.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8" dependencies: @@ -4259,7 +4259,7 @@ hoek@4.x.x: version "4.2.1" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" -hoist-non-react-statics@^3.1.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" dependencies: @@ -6269,8 +6269,8 @@ node-releases@^1.1.3: semver "^5.3.0" node-releases@^1.1.8: - version "1.1.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.13.tgz#8c03296b5ae60c08e2ff4f8f22ae45bd2f210083" + version "1.1.12" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.12.tgz#1d6baf544316b5422fcd35efe18708370a4e7637" dependencies: semver "^5.3.0" @@ -7744,6 +7744,21 @@ react-is@^16.8.1: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" +react-is@^16.8.2: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + +react-redux@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" + dependencies: + "@babel/runtime" "^7.3.1" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.8.2" + react-router-dom@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.0.tgz#542a9b86af269a37f0b87218c4c25ea8dcf0c073" @@ -7933,6 +7948,13 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redux@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" @@ -9033,7 +9055,7 @@ svgo@^1.0.0, svgo@^1.1.1: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.1.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"