Integrating Redux

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.

Learning Objectives

Re-imagining our app using Redux

Let's first visualise how the Redux store will fit in to our project:

animated example of actions being dispatched in Redux

  1. The UI triggers an event
  2. The event handler dispatches an 'action'
  3. The store receives the action and filters it down through all the resolvers looking for a match
  4. A resolver updates the store
  5. Redux triggers a re-render of all the components that have used a selector to select those reactive variables from within the store with the newly updated value

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.

Get on board the refactor tractor 🚜

Here are the steps we need to take our app to the next level.

  1. Install redux
  2. Create the redux store
  3. Establish the initial state of the store
  4. Read values from the store
  5. Create actions to update the store
  6. Dispatch those actions from our components
  7. Check we have maintain all the functionality from before

Install redux

npm install @reduxjs/toolkit react-redux 

We are adding 2 libraries.

Create the redux store

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.

Establish the initial state of the store

Our store has the initial state of the cart (an empty array), and the products.

Read values from the store

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>

Create actions to update the store

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.

Dispatch those actions from our components

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.

  1. useDispatch from 'react-redux'
  2. the action to dispatch from my own redux store

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>
    )
}

Check we have maintain all the functionality from before

You should now be able to remove all references to addToCart from props, we don't need to pass it around any more.

Assignment

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'.

Additional resources

Redux quick start