Now we have introduced the Product Details page, we need a way to access that Product's information so we can display it on the frontend. You've done that. The problem is, passing the whole data.products array into a component that only needs one product is not a scalable approach.
Instead, we need a central place to manage the data state and to give the Product Details component the ability to query the state based on the dynamic ID we pass in the URL.
Let's first visualise how the Redux store will fit in to our project:
Our cart is a good candidate for the store because it is an example of global state. The data of all the data.products is also something that can live in the store as that will save us having to pass it around when we want to read from it.
Here are the steps we need to take our app to the next level.
npm install @reduxjs/toolkit react-redux
We are adding 2 libraries.
This needs to be at the highest level of the application so we are going to go one step above our App component into the index.js
file. In here we will create the redux store and make it avaiable to every component in our app to access.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { configureStore, createSlice } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import data from './products.json'
const initialState = createSlice({
name: 'plants-direct',
initialState: {
cart: [],
products: data.products
},
reducers: {}
})
const store = configureStore(initialState)
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
From the top of the file, we are importing some tools, { configureStore, createSlice }
and a Provider Component from our newly added modules. Next we create a slice of a redux store, this has a name, state and reducers. We then create a redux store object by calling configureStore and then pass this into the Provider component.
Our store has the initial state of the cart (an empty array), and the products.
To read from the store use the useSelector function. Update the Home component to map over the products from the store not from props.
import { Product } from './Product'
import { useSelector } from 'react-redux'
function Home() {
const products = useSelector(state => state.products)
return (
<div>
{products.map(product => <Product key={product.productId} product={product} />)}
</div>
);
}
export default Home;
useSelector returns a reactive variable that is selected from the store's state via a function. We pass useSelector a function that must pick out the values we want to read from and be reactive.
Check this works OK.
In the ProductDetails component you can do the same. Create a selector function that picks out a product from the products in the store i.e. useSelector(state => state.products.find...)
You can now remove the data import from the App component, and remove the way we passed data down though the Home component via props etc.
<Route path="/">
<Home addToCart={addToCart} />
</Route>
We can create actions in a the store's reducer. These actions can update the state in a mutable way. Behind the scenes your mutations are dealt with in an immutable way (but that is for another session!). Your actions will be functions that receive the store's state and if you pass parameters in your actions, they will be available on an object as action.payload.
Here is my refactored addToCart function. I'm making sure I change both the cart and the product in the products array as I want to cause a re-render of everything that is reading these two values.
const initialState = createSlice({
name: 'plants-direct',
initialState: {
cart: [],
products: data.products
},
reducers: {
addToCart: (state, action) => {
const index = state.products.findIndex(p => p.productId === action.payload.productId)
const inCart = state.cart.findIndex(p => p.productId === action.payload.productId)
if (inCart > -1) {
state.products[index].addedToCart = false
state.cart.splice(index, 1)
} else {
state.products[index].addedToCart = true
state.cart.push(action.payload)
}
}
}
})
export const { addToCart } = initialState.actions
🉐 Notice on the last line above I'm exposing the addToCart function so I can call it from elsewhere in the app.
In the UI when I click on the 'Add to cart' button I want to dispatch that action. I need 2 things to dispatch an action.
In your Product component import the above 2 items. Then create an instance of dispatch then call dispatch() with your action called with the right parameter. See the example below.
import { Link } from 'react-router-dom'
import { addToCart } from './index'
import { useDispatch } from 'react-redux'
export const Product = (props) => {
const {
productId,
name,
images,
addedToCart
} = props.product
const dispatch = useDispatch()
return (
<article className="product">
{/* rest of your component code */}
<div className="promo-blocks__actions">
<Link to={`/products/${productId}`}>Full Details</Link>
<button onClick={() => dispatch(addToCart(props.product))}>{addedToCart ? 'Remove from' : 'Add to'} cart</button>
</div>
</article>
)
}
You should now be able to remove all references to addToCart from props, we don't need to pass it around any more.
You should now see the Product Details and be able to add/remove the product to/from the cart! In addition, the state should hold when you return to the home page. Add the product to the cart, return home, and you should still see the item in the cart. You should also see that the product's 'Add to cart' button has changed to 'Remove from cart'.