Using Redux for Quick Prototyping

June 10, 2024
article featured image
There are a lot of examples for using Redux with React, but there are some other items here that may be helpful. This uses Webpack with React and TypeScript and the Redux Toolkit. Plus an idea of what ChatGPT and AI can do to develop scaffolding and help organize your projects.

Table of Contents

    §  Is Redux still relevant?

    A lot of projects are moving away from Redux due to its overbearing complexity and that its capabilties might overkill for many efforts. I think Redux still shines in its ability to show state management and also provide peace-of-mind that your application will never get too complex to be managed.

    As with any React effort, I usually begin with an updated React / TypeScript / Webpack template including Bootstrap 5.

    This sample application takes a number of stock symbols, pulls the data from AlphaVantage and sorts them according to market cap.

    This is a configuration of the store for the entire application:

    // store.ts
    import { configureStore } from '@reduxjs/toolkit';
    import stocksReducer from './stocksSlice';
    import { ThunkAction, Action } from '@reduxjs/toolkit';
    
    export const store = configureStore({
      reducer: {
        stocks: stocksReducer,
      },
    });
    
    // Infer the `RootState` and `AppDispatch` types from the store itself
    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;
    export type AppThunk<ReturnType = void> = ThunkAction<
      ReturnType,
      RootState,
      unknown,
      Action<string>
    >;
    

    As you see, this is a standard use of the Redux Toolkit, allowing for a reducer for our stocks data.

    So with our StocksSlice file, we use Axios to pull the data with an API, and manipulate the data to feed into our application however we like:

    // stocksSlice.ts
    import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
    import axios from 'axios';
    
    const API_KEY = '5XXXXXXXXXXXX';
    const BASE_URL = 'https://www.alphavantage.co/query';
    
    interface CompanyInfo {
      Name: string;
      MarketCapitalization: string;
      '50DayMovingAverage': string;
      ChangePercent: string;
      CurrentPrice: string;
    }
    
    interface StocksState {
      data: Record<string, CompanyInfo>;
      lastUpdated: Record<string, string>;
      loading: boolean;
      error: string | null;
    }
    
    const initialState: StocksState = {
      data: {},
      lastUpdated: {},
      loading: false,
      error: null,
    };
    
    export const fetchStockInfo = createAsyncThunk(
      'stocks/fetchStockInfo',
      async (symbols: string[]) => {
        const companyInfo: Record<string, CompanyInfo> = {};
    
        for (const symbol of symbols) {
          const overviewUrl = `${BASE_URL}?function=OVERVIEW&symbol=${symbol}&apikey=${API_KEY}`;
          const dailyUrl = `${BASE_URL}?function=TIME_SERIES_DAILY&symbol=${symbol}&apikey=${API_KEY}`;
    
          try {
            const [overviewResponse, dailyResponse] = await Promise.all([
              axios.get(overviewUrl),
              axios.get(dailyUrl),
            ]);
    
            const overviewData = overviewResponse.data;
            const dailyData = dailyResponse.data['Time Series (Daily)'];
    
            const lastTwoDays = Object.keys(dailyData).slice(0, 2);
            const latestDay = dailyData[lastTwoDays[0]];
            const previousDay = dailyData[lastTwoDays[1]];
    
            const latestClose = parseFloat(latestDay['4. close']);
            const previousClose = parseFloat(previousDay['4. close']);
    
            const changePercent = (
              ((latestClose - previousClose) / previousClose) *
              100
            ).toFixed(2);
    
            companyInfo[symbol] = {
              ...overviewData,
              ChangePercent: changePercent + '%',
              CurrentPrice: latestClose.toFixed(2), // Include current price
            };
          } catch (error) {
            console.error(
              `Error fetching data for ${symbol}:`,
              (error as Error).message,
            );
          }
        }
    
        return companyInfo;
      },
    );
    
    const stocksSlice = createSlice({
      name: 'stocks',
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder
          .addCase(fetchStockInfo.pending, (state) => {
            state.loading = true;
            state.error = null;
          })
          .addCase(fetchStockInfo.fulfilled, (state, action) => {
            state.loading = false;
            const now = new Date().toISOString();
            Object.entries(action.payload).forEach(([symbol, info]) => {
              state.data[symbol] = info;
              state.lastUpdated[symbol] = now;
            });
          })
          .addCase(fetchStockInfo.rejected, (state, action) => {
            state.loading = false;
            state.error = action.error.message || 'Failed to fetch stock data';
          });
      },
    });
    
    export default stocksSlice.reducer;

    To get this up and running quickly, we'll open up a ChatGPT discussion and ask it to put together a series of Bootstrap cards with stock data. ChatGPT nicely creates something simple for us, which we ultimately use in our TSX page:

    // Stocks.tsx
    import React, { useEffect, useState } from 'react';
    import DefaultLayout from 'layouts/default/DefaultLayout';
    import { Tab, Tabs, Spinner } from 'react-bootstrap';
    import { useDispatch, useSelector } from 'react-redux';
    import { Helmet } from 'react-helmet';
    import { fetchStockInfo } from 'store/stocksSlice';
    import { RootState, AppDispatch } from 'store/store';
    import { format } from 'date-fns';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import 'styles/styles.scss';
    
    // Helper function to format the market capitalization
    const formatMarketCap = (marketCap: string): string => {
      if (!marketCap) return 'N/A';
    
      // Remove any non-numeric characters (e.g., commas)
      const num = parseFloat(marketCap.replace(/[^0-9.-]+/g, ''));
    
      if (isNaN(num)) {
        return 'N/A';
      }
    
      if (num >= 1e12) {
        return `$${(num / 1e12).toFixed(2)}T`;
      } else if (num >= 1e9) {
        return `$${(num / 1e9).toFixed(2)}B`;
      } else if (num >= 1e6) {
        return `$${(num / 1e6).toFixed(2)}M`;
      } else {
        return `$${num.toFixed(2)}`;
      }
    };
    
    // Helper function to format dates
    const formatDate = (date: string): string => {
      const parsedDate = new Date(date);
      return format(parsedDate, 'yy-MM-dd h:mmaaa');
    };
    
    const Stocks: React.FC = () => {
      const dispatch: AppDispatch = useDispatch();
      const { data, lastUpdated, loading, error } = useSelector(
        (state: RootState) => state.stocks,
      );
    
      const [sortMethod, setSortMethod] = useState('marketCap');
    
      useEffect(() => {
        const symbols = [
          'AAPL',
          'GOOGL',
          'MSFT',
          'AMZN',
          'META',
          'NVDA',
          'TSLA',
          'NFLX',
          'AMD',
          'INTC',
          'PYPL',
          'CRM',
          'ADBE',
          'ORCL',
          'ZM',
        ];
    
        dispatch(fetchStockInfo(symbols));
      }, [dispatch]);
    
      const containerCSS = 'homemade-container-sm mx-auto w-100 d-flex flex-column';
    
      const sortedData = Object.entries(data).sort((a, b) => {
        if (sortMethod === 'marketCap') {
          const marketCapA =
            parseFloat(a[1].MarketCapitalization.replace(/[^0-9.-]+/g, '')) || 0;
          const marketCapB =
            parseFloat(b[1].MarketCapitalization.replace(/[^0-9.-]+/g, '')) || 0;
          return marketCapB - marketCapA;
        } else {
          return a[1].Name.localeCompare(b[1].Name);
        }
      });
    
      return (
        <DefaultLayout>
          <div className={containerCSS}>
            <Helmet>
              <html lang="en" />
              <title>Stocks</title>
              <meta name="description" content="Basic example" />
            </Helmet>
            <h1>Stocks</h1>
            {loading && (
              <div className="m-auto">
                <Spinner animation="border" role="status" className="my-3">
                  <span className="visually-hidden">Loading...</span>
                </Spinner>
              </div>
            )}
            {error && <p>Error: {error}</p>}
            <Tabs
              id="stock-sort-tabs"
              activeKey={sortMethod}
              onSelect={(k) => setSortMethod(k || 'marketCap')}
              className="mb-3"
            >
              <Tab eventKey="marketCap" title="Sort by Market Cap">
                <div className="d-flex flex-wrap">
                  {sortedData.map(([symbol, info]) => {
                    const priceChange =
                      parseFloat(info.ChangePercent.replace(/[^0-9.-]+/g, '')) || 0;
                    const isPositive = priceChange >= 0;
    
                    return (
                      <div
                        key={symbol}
                        className="card custom-card position-relative"
                      >
                        <div
                          className={`circle-indicator ${
                            isPositive ? 'positive' : 'negative'
                          }`}
                        ></div>
                        <div className="card-body">
                          <h5 className="card-title">{symbol}</h5>
                          <h6 className="card-subtitle mb-2 text-muted">
                            {info.Name}
                          </h6>
                          <p className="card-text">
                            <strong>Market Cap:</strong>{' '}
                            {formatMarketCap(info.MarketCapitalization)}
                            <br />
                            <strong>Stock Price:</strong> {info.CurrentPrice}
                            <br />
                            {lastUpdated[symbol] && (
                              <small className="text-muted">
                                Last updated: {formatDate(lastUpdated[symbol])}
                              </small>
                            )}
                          </p>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </Tab>
              <Tab eventKey="alphabetical" title="Sort Alphabetically">
                <div className="d-flex flex-wrap">
                  {sortedData.map(([symbol, info]) => {
                    const priceChange =
                      parseFloat(info.ChangePercent.replace(/[^0-9.-]+/g, '')) || 0;
                    const isPositive = priceChange >= 0;
    
                    return (
                      <div
                        key={symbol}
                        className="card custom-card position-relative"
                      >
                        <div
                          className={`circle-indicator ${
                            isPositive ? 'positive' : 'negative'
                          }`}
                        ></div>
                        <div className="card-body">
                          <h5 className="card-title">{symbol}</h5>
                          <h6 className="card-subtitle mb-2 text-muted">
                            {info.Name}
                          </h6>
                          <p className="card-text">
                            <strong>Market Cap:</strong>{' '}
                            {formatMarketCap(info.MarketCapitalization)}
                            <br />
                            <strong>Stock Price:</strong>{' '}
                            {info['50DayMovingAverage']}
                            <br />
                            {lastUpdated[symbol] && (
                              <small className="text-muted">
                                Last updated: {formatDate(lastUpdated[symbol])}
                              </small>
                            )}
                          </p>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </Tab>
            </Tabs>
          </div>
        </DefaultLayout>
      );
    };
    
    export default Stocks;
    

    The application in its simplest form now looks like this:

    Image Library

    §  Rapid Prototyping with AI

    This is a pretty simple example of using React/Redux, but what surprised me was how much of this code I could generate with AI tools. It led me to ask some new questions:

    • How much does it make sense to still do prototyping in drawing applications such as Figma and Sketch?
    • How quickly can we present a concept to stakeholders and users?
    • What kind of digital assets can UX designers now provide developers?

    §  Conclusion

    I'm sure I and many other front-end developers have spent significant amounts of time honing our skills and it hurts a little to see what would have been a complex effort done in a shorter amount of time. It still requires skill to review generated code and identify how effectively the underlying technologies are working and correcting where it is wrong or inefficient. I'm certainly hopeful that AI will ultimately lead us to better apps and provide us with more effective user experiences.

    Github Repo: Stocks Redux