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
+
+ {this.props.items.map(item => (
+ -
+ {item.product.name} - {item.quantity}
+
+ ))}
+
+
+ )
+ }
+}
+
+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"