Nick De Jesus
I like React, love React Native. I play Tekken competitively
by Nick DeJesus
Building a Stripe Powered Shopping Cart
Someone asked me if they could use this library with HTML and JS only
I said, "No".
:(
I originally wanted to be an Android developer
Releasing an app on Android was great, but many iOS users were feeling FOMO
This ultimately lead me to becoming a web developer.
These same feelings motivated me to make an attempt at making USC framework-agnostic
State management libraries are framework agnostic
The goal is to remake the functionality of the hooks but with a state management library of my choice.
Redux Then
Redux Now
Redux is based on a well-known standard called Flux.
However, the library itself is not opinionated at all. This left a lot of room for an ecosystem of different tools and approaches to come into play.
This made redux very hard learn. While there are many "best practices", every redux implementation could be wildly different from the other.
"One Way Data Flow"
Redux Tool Kit is the official opinionated version of redux. It comes with methods and tools that come from industry best practices.
Everything that made people so frustrated with redux is addressed by RTK
(I happened to also change the API a bit during this process)
An object full of reducer functions that automatically generates action creators and action types that correspond to the reducers and state.
const slice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: {
reducer: (state, { payload: { product, options } }) => { /* LOGIC */ },
prepare: (product, options = { count: 1 }) => { /* LOGIC */ }
}
}
})
Prepare
Prepare gives you access to the params passed to actions before sending them as the actions payload.
(with redux-only you needed a lib for this)
function Entry(
product,
quantity,
currency,
language,
price_metadata,
product_metadata
) {
/* Things happen here */
return {
...product,
id,
quantity,
get value() {
return this.price * this.quantity
},
get formattedValue() {
return formatCurrencyString({
value: this.value,
currency,
language
})
}
}
}
(A little context)
import { configureStore } from '@reduxjs/toolkit'
import { reducer, initialState } from './slice'
export function createShoppingCartStore(options) {
return configureStore({
reducer,
preloadedState: { ...initialState, ...options }
})
}
With redux, you need to create stores for your apps. We had to make a creator function for creating stores with an options param for configuration.
rollup build cmd
This is no longer a hooks library 🤔
We need to create a wrapper around the core implementation that allows you to interact with it through hooks
export function CartProvider({ children, ...props }) {
const store = React.useMemo(() => createShoppingCartStore(props), [props])
return (
<Provider context={CartContext} store={store} value={store}>
{children}
</Provider>
)
}
export function useShoppingCart(
selector = (state) => ({ ...state }),
equalityFn
) {
const dispatch = useDispatch()
const cartState = useSelector(selector, equalityFn)
const cartActions = bindActionCreators(actions, dispatch)
const newState = { ...cartState, ...cartActions }
React.useDebugValue(newState)
return newState
}
import { jsx } from 'theme-ui'
import ReactDOM from 'react-dom'
import { loadStripe } from '@stripe/stripe-js'
import { CartProvider } from 'use-shopping-cart'
import './index.css'
import App from './App'
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_API_PUBLIC)
ReactDOM.render(
<CartProvider
mode="checkout-session"
stripe={stripePromise}
billingAddressCollection={false}
successUrl="https://stripe.com"
cancelUrl="https://twitter.com/dayhaysoos"
currency="USD"
>
<App />
</CartProvider>,
document.getElementById('root')
)
/** @jsx jsx */
import { jsx } from 'theme-ui'
import ReactDOM from 'react-dom'
import { CartProvider } from 'use-shopping-cart'
import './index.css'
import App from './App'
ReactDOM.render(
<CartProvider
mode="payment"
cartMode="checkout-session"
stripe={process.env.REACT_APP_STRIPE_API_PUBLIC}
billingAddressCollection={false}
successUrl="https://stripe.com"
cancelUrl="https://twitter.com/dayhaysoos"
currency="USD"
>
<App />
</CartProvider>,
document.getElementById('root')
)
Middleware allows you to customize the dispatch function. Remember, dispatch is what you use to call your actions.
We use middleware on USC for:
export const handleWarnings = (store) => (next) => async (action) => {
switch (action.type) {
// These have `count`
case 'cart/addItem':
case 'cart/incrementItem':
case 'cart/decrementItem':
if (!isValidCount(action.payload?.options?.count, action)) return
break
// This one has `quantity`
case 'cart/setItemQuantity':
if (!isValidCount(action.payload?.quantity, action)) return
break
default:
break
}
return next(action)
}
Warning Middleware
Stripe Middleware
export const handleStripe = (store) => (next) => async (action) => {
const stripePublicKey = store.getState().stripe
const cart = store.getState()
let stripe
if (action.type === 'cart/redirectToCheckout') {
try {
stripe = await loadStripe(stripePublicKey)
} catch (error) {
console.log('error', error)
}
if (cart.cartMode === 'checkout-session') {
return stripe.redirectToCheckout({
sessionId: action.payload.sessionId
})
}
if (cart.cartMode === 'client-only') {
const checkoutData = getCheckoutData(cart)
stripe.redirectToCheckout(checkoutData)
}
throw new Error(`Invalid value for "cartMode" was found: ${cart.cartMode}`)
}
}
When updating redux, you're not allowed to directly mutate state, you have to return a new state.
Immer allows you to "mutate state" by letting you update a draft version of the state. It handles the returning of the next state for you.
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script
crossorigin
src="https://unpkg.com/use-shopping-cart@3.0.0-beta.3/dist/core.umd.js"
></script>
let store = UseShoppingCartCore.createShoppingCartStore({
stripe: testApiKey,
mode: "client-only",
successUrl: "https://twitter.com/dayhaysoos",
cancelUrl: "https://stripe.com",
});
redux-persist is a middleware that makes sure your data is in sync with local storage.
Also compatible with React Native!
export function createShoppingCartStore(options) {
const persistConfig = {
key: 'root',
version: 1,
storage,
whitelist: ['cartCount', 'totalPrice', 'formattedTotalPrice', 'cartDetails']
}
const persistedReducer = persistReducer(persistConfig, reducer)
const newInitialState = { ...initialState, ...options }
updateFormattedTotalPrice(newInitialState)
return configureStore({
reducer: persistedReducer,
preloadedState: newInitialState,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
}).concat(handleStripe, handleWarnings)
})
}
export function CartProvider({ loading = null, children, ...props }) {
const store = React.useMemo(() => createShoppingCartStore(props), [props])
const persistor = persistStore(store)
return (
<Provider context={CartContext} store={store}>
<PersistGate
persistor={persistor}
children={(bootstrapped) => {
if (!bootstrapped) return loading
return children
}}
/>
</Provider>
)
}
By Nick De Jesus