Effortless Data Fetching and Rendering with Redux Toolkit
Tags: redux, react, javascript, webdev
Published: 2023-11-08 09:52:45

What could be more fun than getting to know our tools by rolling up our sleeves and using them? So, come on—let’s not spend another second! Let’s jump in and start playing around with the code together.

First, let's outline the endpoints we'll be working with. Imagine these endpoints are crafted in a backend language (for instance, I've set up an Express API for this purpose).

#### constants.js

```javascript
export const BASE_URL = ''
export const PRODUCTS_URL = '/api/products'
```
Let's go ahead and construct our apiSlice. You might be wondering, what exactly is a slice? To borrow from the Redux documentation:

💡Redux Slices: Think of a "slice" as a segment of Redux that contains both the reducer logic and actions for a specific feature within your application, usually housed within a single file. This concept is akin to slicing the overall Redux state object into several "slices" of state, each managing its own domain.

💡The `createApi` function is the essence of what makes RTK Query tick. It empowers you to set up various "endpoints" that articulate the methods for fetching data from backend APIs and asynchronous sources. This includes specifying the procedures for both retrieving and reshaping that data. What you get as a result is an "API slice," which is a structured collection of Redux logic — with the added bonus of React hooks, if you opt for them — that takes care of the data fetching and caching for you.

As it stands, we have yet to define any endpoints. However, as we build each slice moving forward, we will dynamically incorporate endpoints into the apiSlice by using `injectEndpoints`.
#### apiSlice.js

```javascript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { BASE_URL } from '../constants'

const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL })

export const apiSlice = createApi({
    baseQuery,
    tagTypes: ['Product', 'Order', 'User'],
    endpoints: (builder) => ({})
});
```
#### Creating the Redux Store

💡To create the Redux store, we utilize the configureStore function provided by the Redux Toolkit. This function mandates that we supply a reducer parameter.

💡Consider our application as a collection of various features, each potentially having its own distinct reducer function. When invoking configureStore, we have the ability to input these distinct reducers as an object. The keys within this object will correspond to the keys in our ultimate state value.

#### store.js

```javascript

import { configureStore } from '@reduxjs/toolkit'
import { apiSlice } from './slices/apiSlice'

const store = configureStore({
    reducer: {
        [apiSlice.reducerPath]: apiSlice.reducer
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(apiSlice.middleware),
    devTools: true
})

export default store
```
Now, let's proceed to set up the productApiSlice for data retrieval purposes.

This productApiSlice is designed to handle two specific endpoints: `getProducts`, which retrieves a list of products, and `getProductDetails`, which obtains the details of a single product by its ID.

📌Take note that we have not resorted to using any external libraries (like axios or the fetch API) to perform the API calls. This is taken care of by the Redux Toolkit itself.

By utilizing `apiSlice.injectEndpoints`, we're integrating these two endpoints into the apiSlice.

📌It's also worth highlighting the naming convention used for the exports: `useGetProductsQuery`, `useGetProductDetailsQuery`.

The names are derived from the respective endpoints, converted to camelCase, and prefixed with `use` and suffixed with `Query` to conform to the conventions.

#### productsApiSlice.js

```javascript

import { PRODUCTS_URL } from "../constants";
import { apiSlice } from "./apiSlice";

export const productsApiSlice = apiSlice.injectEndpoints({
    endpoints: (builder) => ({
        getProducts: builder.query({
            query: () => ({
                url: PRODUCTS_URL
            }),
            keepUnusedDataFor: 5
        }),
        getProductDetails: builder. Query({
            query: (productId) => ({
                url: `${PRODUCTS_URL}/${productId}`,
            }),
            keepUnusedDataFor: 5
        })
    })
})

export const { useGetProductsQuery, useGetProductDetailsQuery } = productsApiSlice
```
Now, let's go ahead and construct the product component, which will be responsible for rendering a product's details.

#### product.jsx

```javascript
import React from 'react'
import { Card } from 'react-bootstrap'
import { Link } from 'react-router-dom'
import Rating from './Rating'

const Product = ({ product }) => {

    return (
        <Card className='my-3 p-3 rounded'>
            <Link to={`/product/${product._id}`}>
                <Card.Img src={product.image} variant='top' />
            </Link>
            <Card.Body>
                <Link to={`/product/${product._id}`}>
                    <Card.Title as='div' className='product-title'>
                        <strong>{product.name}</strong>
                    </Card.Title>
                </Link>
                <Card.Text as='div'>
                    <Rating value={product.rating} text={`${product.numReviews} reviews`} />
                </Card.Text>

                <Card.Text as='h3'>
                    ${product.price}
                </Card.Text>
            </Card.Body>
        </Card>
    )
}

export default Product
```

Now, let's retrieve and display the products by utilizing the endpoints from productsApiSlice.

In HomeScreen.jsx, we can acquire the products with a single line of code by using `useGetProductsQuery`:
```jsx
const { data: products, isLoading, error } = useGetProductsQuery();
```

The data we retrieve will be stored in a variable called products. The isLoading variable remains true during the data fetching process and switches to false once the data is loaded. Should there be any errors, the error variable is designed to capture them, eliminating the need for a try-catch block.

#### HomeScreen.jsx

```javascript
import React from 'react'
import { Row, Col } from 'react-bootstrap'
import Product from '../components/Product'
import Loader from '../components/Loader'
import Message from '../components/Message'

import { useGetProductsQuery } from '../slices/productsApiSlice'

const HomeScreen = () => {
  const { data: products, isLoading, error } = useGetProductsQuery()
  return (
    <>
      {isLoading ? (
        <Loader />
      ) : error ? (
        <Message variant='danger'>{error?.data?.message || error.error}</Message>
      ) : (
        <>
          <h1>Latest Products</h1>
          <Row>
            {products.map((product) => {
              return (
                <Col key={product._id} sm={12} md={6} lg={4} xl={3}>
                  <Product product={product} />
                </Col>
              )
            })}
          </Row>
        </>
      )}
    </>
  )
}

export default HomeScreen

```
Now, let's develop the Product screen, which will be dedicated to rendering the details of a product.

#### ProductScreen.jsx

```javascript
import React, { useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { Form, Row, Col, Image, ListGroup, Card, Button } from 'react-bootstrap'
import Rating from '../components/Rating'
import Loader from '../components/Loader'
import Message from '../components/Message'

import { useGetProductDetailsQuery } from '../slices/productsApiSlice'

const ProductScreen = () => {
  let { id: productId } = useParams()
  let navigate = useNavigate()
  const [qty, setQty] = useState(1)
  const { data: product, isLoading, error } = useGetProductDetailsQuery(productId)

  const addToCartHandler = () => {
    navigate(`/cart/${productId}?qty=${qty}`)
  }

  return (
    <>
      <Link className='btn btn-light my-3' to='/'>
        Go Back
      </Link>
      {isLoading ? (
        <Loader />
      ) : error ? (
        <Message variant='danger'>{error?.data?.message || error.error}</Message>
      ) : (
        <Row>
          <Col md={5}>
            <Image src={product.image} alt={product.name} fluid></Image>
          </Col>
          <Col md={4}>
            <ListGroup variant='flush'>
              <ListGroup.Item>
                <h3>{product.name}</h3>
              </ListGroup.Item>
              <ListGroup.Item>
                <Rating value={product.rating ? product.rating : 0} text={`${product.numReviews} reviews`} />
              </ListGroup.Item>
              <ListGroup.Item>Price: ${product.price}</ListGroup.Item>
              <ListGroup.Item>Description: {product.description}</ListGroup.Item>
            </ListGroup>
          </Col>
          <Col md={3}>
            <Card>
              <ListGroup variant='flush'>
                <ListGroup.Item>
                  <Row>
                    <Col>Price:</Col>
                    <Col>
                      <strong>${product.price}</strong>
                    </Col>
                  </Row>
                </ListGroup.Item>
                <ListGroup.Item>
                  <Row>
                    <Col>Status:</Col>
                    <Col>
                      <strong> {product.countInStock > 0 ? 'In stock' : 'Out of stock'}</strong>
                    </Col>
                  </Row>
                </ListGroup.Item>

                {product.countInStock > 0 && (
                  <ListGroup.Item>
                    <Row>
                      <Col>Qty</Col>
                      <Col>
                        <Form.Control as='select' value={qty} onChange={(e) => setQty(e.target.value)}>
                          {[...Array(product.countInStock).keys()].map((x) => (
                            <option key={x + 1} value={x + 1}>
                              {x + 1}
                            </option>
                          ))}
                        </Form.Control>
                      </Col>
                    </Row>
                  </ListGroup.Item>
                )}

                <ListGroup.Item>
                  <Button onClick={addToCartHandler} className='btn-block' disabled={product.countInStock === 0}>
                    Add To Cart
                  </Button>
                </ListGroup.Item>
              </ListGroup>
            </Card>
          </Col>
        </Row>
      )}
    </>
  )
}

export default ProductScreen

```

With all the pieces in place—from defining our endpoints in the productApiSlice to effortlessly fetching product listings and details with our hooks—it's time to bring it all together on the Product screen. Here we'll take the data we've obtained and craft a user interface that not only showcases product details but also enriches the user experience. It's a testament to the streamlined process and efficiency of using Redux Toolkit in our project, encapsulating complex functionality into readable and maintainable code. As we render each product detail into our Product screen, we can appreciate the seamless integration of data handling and UI rendering that our setup provides.