npx create-react-app my-app --template redux
- @latest
npx create-react-app@latest my-app --template redux
npm install @reduxjs/toolkit react-redux
consists of few libraries
- redux (core library, state management)
- immer (allows to mutate state)
- redux-thunk (handles async actions)
- reselect (simplifies reducer functions)
- redux devtools
- combine reducers
connects our app to redux
- create store.js
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {},
});
- index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// import store and provider
import { store } from './store';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
- application feature
- create features folder/cart
- create cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
cartItems: [],
amount: 0,
total: 0,
isLoading: true,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
});
console.log(cartSlice);
export default cartSlice.reducer;
- store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './features/cart/cartSlice';
export const store = configureStore({
reducer: {
cart: cartReducer,
},
});
- extension
- create components/Navbar.js
import { CartIcon } from '../icons';
import { useSelector } from 'react-redux';
const Navbar = () => {
const { amount } = useSelector((state) => state.cart);
return (
<nav>
<div className='nav-center'>
<h3>redux toolkit</h3>
<div className='nav-container'>
<CartIcon />
<div className='amount-container'>
<p className='total-amount'>{amount}</p>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;
nav svg {
width: 40px;
color: var(--clr-white);
}
- cartSlice.js
import cartItems from '../../cartItems';
const initialState = {
cartItems: cartItems,
amount: 0,
total: 0,
isLoading: true,
};
- create CartContainer.js and CartItem.js
- CartContainer.js
import React from 'react';
import CartItem from './CartItem';
import { useSelector } from 'react-redux';
const CartContainer = () => {
const { cartItems, total, amount } = useSelector((state) => state.cart);
if (amount < 1) {
return (
<section className='cart'>
{/* cart header */}
<header>
<h2>your bag</h2>
<h4 className='empty-cart'>is currently empty</h4>
</header>
</section>
);
}
return (
<section className='cart'>
{/* cart header */}
<header>
<h2>your bag</h2>
</header>
{/* cart items */}
<div>
{cartItems.map((item) => {
return <CartItem key={item.id} {...item} />;
})}
</div>
{/* cart footer */}
<footer>
<hr />
<div className='cart-total'>
<h4>
total <span>${total}</span>
</h4>
</div>
<button className='btn clear-btn'>clear cart</button>
</footer>
</section>
);
};
export default CartContainer;
- CartItem.js
import React from 'react';
import { ChevronDown, ChevronUp } from '../icons';
const CartItem = ({ id, img, title, price, amount }) => {
return (
<article className='cart-item'>
<img src={img} alt={title} />
<div>
<h4>{title}</h4>
<h4 className='item-price'>${price}</h4>
{/* remove button */}
<button className='remove-btn'>remove</button>
</div>
<div>
{/* increase amount */}
<button className='amount-btn'>
<ChevronUp />
</button>
{/* amount */}
<p className='amount'>{amount}</p>
{/* decrease amount */}
<button className='amount-btn'>
<ChevronDown />
</button>
</div>
</article>
);
};
export default CartItem;
- cartSlice.js
- Immer library
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
clearCart: (state) => {
state.cartItems = [];
},
},
});
export const { clearCart } = cartSlice.actions;
- create action
const ACTION_TYPE = 'ACTION_TYPE';
const actionCreator = (payload) => {
return { type: ACTION_TYPE, payload: payload };
};
- CartContainer.js
import React from 'react';
import CartItem from './CartItem';
import { useDispatch, useSelector } from 'react-redux';
const CartContainer = () => {
const dispatch = useDispatch();
return (
<button
className='btn clear-btn'
onClick={() => {
dispatch(clearCart());
}}
>
clear cart
</button>
);
};
export default CartContainer;
- cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
import cartItems from '../../cartItems';
const initialState = {
cartItems: [],
amount: 0,
total: 0,
isLoading: true,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
clearCart: (state) => {
state.cartItems = [];
},
removeItem: (state, action) => {
const itemId = action.payload;
state.cartItems = state.cartItems.filter((item) => item.id !== itemId);
},
increase: (state, { payload }) => {
const cartItem = state.cartItems.find((item) => item.id === payload.id);
cartItem.amount = cartItem.amount + 1;
},
decrease: (state, { payload }) => {
const cartItem = state.cartItems.find((item) => item.id === payload.id);
cartItem.amount = cartItem.amount - 1;
},
calculateTotals: (state) => {
let amount = 0;
let total = 0;
state.cartItems.forEach((item) => {
amount += item.amount;
total += item.amount * item.price;
});
state.amount = amount;
state.total = total;
},
},
});
export const { clearCart, removeItem, increase, decrease, calculateTotals } =
cartSlice.actions;
export default cartSlice.reducer;
- CartItem.js
import React from 'react';
import { ChevronDown, ChevronUp } from '../icons';
import { useDispatch } from 'react-redux';
import { removeItem, increase, decrease } from '../features/cart/cartSlice';
const CartItem = ({ id, img, title, price, amount }) => {
const dispatch = useDispatch();
return (
<article className='cart-item'>
<img src={img} alt={title} />
<div>
<h4>{title}</h4>
<h4 className='item-price'>${price}</h4>
{/* remove button */}
<button
className='remove-btn'
onClick={() => {
dispatch(removeItem(id));
}}
>
remove
</button>
</div>
<div>
{/* increase amount */}
<button
className='amount-btn'
onClick={() => {
dispatch(increase({ id }));
}}
>
<ChevronUp />
</button>
{/* amount */}
<p className='amount'>{amount}</p>
{/* decrease amount */}
<button
className='amount-btn'
onClick={() => {
if (amount === 1) {
dispatch(removeItem(id));
return;
}
dispatch(decrease({ id }));
}}
>
<ChevronDown />
</button>
</div>
</article>
);
};
export default CartItem;
- App.js
import { useEffect } from 'react';
import Navbar from './components/Navbar';
import CartContainer from './components/CartContainer';
import { useSelector, useDispatch } from 'react-redux';
import { calculateTotals } from './features/cart/cartSlice';
function App() {
const { cartItems } = useSelector((state) => state.cart);
const dispatch = useDispatch();
useEffect(() => {
dispatch(calculateTotals());
}, [cartItems]);
return (
<main>
<Navbar />
<CartContainer />
</main>
);
}
export default App;
- create components/Modal.js
const Modal = () => {
return (
<aside className='modal-container'>
<div className='modal'>
<h4>Remove all items from your shopping cart?</h4>
<div className='btn-container'>
<button type='button' className='btn confirm-btn'>
confirm
</button>
<button type='button' className='btn clear-btn'>
cancel
</button>
</div>
</div>
</aside>
);
};
export default Modal;
- App.js
return (
<main>
<Modal />
<Navbar />
<CartContainer />
</main>
);
- create features/modal/modalSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isOpen: false,
};
const modalSlice = createSlice({
name: 'modal',
initialState,
reducers: {
openModal: (state, action) => {
state.isOpen = true;
},
closeModal: (state, action) => {
state.isOpen = false;
},
},
});
export const { openModal, closeModal } = modalSlice.actions;
export default modalSlice.reducer;
- App.js
const { isOpen } = useSelector((state) => state.modal);
return (
<main>
{isOpen && <Modal />}
<Navbar />
<CartContainer />
</main>
);
- CartContainer.js
import { openModal } from '../features/modal/modalSlice';
return (
<button
className='btn clear-btn'
onClick={() => {
dispatch(openModal());
}}
>
clear cart
</button>
);
- Modal.js
import { closeModal } from '../features/modal/modalSlice';
import { useDispatch } from 'react-redux';
import { clearCart } from '../features/cart/cartSlice';
const Modal = () => {
const dispatch = useDispatch();
return (
<aside className='modal-container'>
<div className='modal'>
<h4>Remove all items from your shopping cart?</h4>
<div className='btn-container'>
<button
type='button'
className='btn confirm-btn'
onClick={() => {
dispatch(clearCart());
dispatch(closeModal());
}}
>
confirm
</button>
<button
type='button'
className='btn clear-btn'
onClick={() => {
dispatch(closeModal());
}}
>
cancel
</button>
</div>
</div>
</aside>
);
};
export default Modal;
-
cartSlice.js
-
action type
-
callback function
-
lifecycle actions
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
const url = 'https://course-api.com/react-useReducer-cart-project';
export const getCartItems = createAsyncThunk('cart/getCartItems', () => {
return fetch(url)
.then((resp) => resp.json())
.catch((err) => console.log(error));
});
const cartSlice = createSlice({
name: 'cart',
initialState,
extraReducers: {
[getCartItems.pending]: (state) => {
state.isLoading = true;
},
[getCartItems.fulfilled]: (state, action) => {
console.log(action);
state.isLoading = false;
state.cartItems = action.payload;
},
[getCartItems.rejected]: (state) => {
state.isLoading = false;
},
},
});
- App.js
import { calculateTotals, getCartItems } from './features/cart/cartSlice';
function App() {
const { cartItems, isLoading } = useSelector((state) => state.cart);
useEffect(() => {
dispatch(getCartItems());
}, []);
if (isLoading) {
return (
<div className='loading'>
<h1>Loading...</h1>
</div>
);
}
return (
<main>
{isOpen && <Modal />}
<Navbar />
<CartContainer />
</main>
);
}
export default App;
npm install axios
- cartSlice.js
export const getCartItems = createAsyncThunk(
'cart/getCartItems',
async (name, thunkAPI) => {
try {
// console.log(name);
// console.log(thunkAPI);
// console.log(thunkAPI.getState());
// thunkAPI.dispatch(openModal());
const resp = await axios(url);
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue('something went wrong');
}
}
);
cart/cartSlice
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
// reducers
},
extraReducers: (builder) => {
builder
.addCase(getCartItems.pending, (state) => {
state.isLoading = true;
})
.addCase(getCartItems.fulfilled, (state, action) => {
// console.log(action);
state.isLoading = false;
state.cartItems = action.payload;
})
.addCase(getCartItems.rejected, (state, action) => {
console.log(action);
state.isLoading = false;
});
},
});