Now that I've touched on how some hooks work in the previous post I'll explain my actual project. I had it ready to go before writing that post, but after I wrote it I realized I wasn't even taking full advantage of the hooks I was using. I guess there really is something to writing these posts that helps to develop a better understanding of these technologies afterall. π
As I was refactoring the project I somehow completely broke it. I would add an item to the cart and it would work fine, but if I added another of the same type it would add 2 more instead of 1. After some googling I determined the issue to be with <React.StrictMode>
which is wrapped around <App />
in index.js
.
The purpose of StrictMode
is to highlight potential problems and detect unexpected side effects. It works in development mode only, and causes your components to render twice. When I remove StrictMode
from my app it works as intended so clearly this is the culprit. I'm still unsure of why I'm getting the unintended side effect of it adding a quantity of 2 to the item the second time, yet not the first time. I'm going to have to continue debugging this, but in the meantime I removed StrictMode
and it works. π
App Organization
In the src
folder I have all of my components separated in their own folders inside a components
folder. Each folder contains a .js
and .css
file for the corresponding component, as seen in the Store
folder in the image above. In the reducers
folder, there are files for each useReducer
in my app. I'm using two: One handles adding, updating, and removing items from the cart, and the other handles opening and closing the modal as well as keeping track of the item that was clicked. The helpers
folder contains a file called constants
, which holds the const
objects I'm using, and cartHelpers
holds the logic for doing all the cart editing and doing the math for the cart total.
How it Works
I decided not use App.js
for my main logic because I have a footer
on the page, so App
just looks like this:
const App = () => (
<div className="App">
<Store />
<Footer />
</div>
);
Store.js
is where my main logic is. In hindsight this name might be confusing, because the word 'store' is associated with reducers as a container for state, but in this instance it's my item shop. I guess I should've just called it Shop. π€¦π»ββοΈ I might go back and change that...
Store
holds the two reducers mentioned earlier:
const [cart, dispatchCart] = useReducer(cartReducer, []);
const [itemClicked, dispatchItemClicked] = useReducer(itemClickedReducer, { isModalVisible: false, modalType: null, item: null });
cart
is initialized to an empty array, and itemClicked
is initialized as an object with a few properties: isModalVisible
controls when the add/remove item modal is displayed, modalType
controls whether it's for adding or removing an item, and item
stores the item object when an item in clicked in either Inventory
or Cart
.
I considered separating the modal stuff from the item clicked, but the modal needs to know about the item to display its info, and when the form in the modal is submitted that's when dispatchCart
runs to either add or remove that item, so to me it makes sense to keep them grouped together.
There are a few functions inside Store
:
handleSubmitItem
is passed to HowManyModal
(the modal with the form to add x amount of an item to the cart) and receives qty
once the modal form is submitted. Since handleSubmitItem
is inside Store
it knows about the itemClicked
. It checks if the modalType
is MODAL.ADD
or MODAL.REMOVE
and sets a const fn
to the appropriate function. fn
is run with the item and quantity.
MODAL.ADD
and MODAL.REMOVE
are just constants to make it easier to read and safer than writing strings that might be typed incorrectly. My actions to send to the dispatcher are also stored as constants.
// constants.js
export const ACTIONS = {
SET: 'set',
CLEAR: 'clear',
ADD_TO_CART: 'add-to-cart',
REMOVE_FROM_CART: 'remove-from-cart',
UPDATE_QUANTITY: 'update-quantity'
}
export const MODAL = {
ADD: 'add',
REMOVE: 'remove'
}
// Store.js
const Store = () => {
// reducers, other functions...
const handleSubmitItem = (qty) => {
const fn = itemClicked.modalType === MODAL.ADD ?
handleAddToCart : handleRemoveFromCart;
fn(itemClicked.item, qty);
};
// ... etc
}
If adding, handleAddToCart
is the function that's run. It checks to see if the item already exists in the cart. If so, dispatchCart
is run with the type
ACTIONS.UPDATE_QUANTITY
, otherwise it's run with type
ACTIONS.ADD_TO_CART
.
// Store.js
const Store = () => {
// reducers, other functions...
const handleAddToCart = (item, qty) => {
const itemExists = cart.find(i => i.name === item);
const type = itemExists ? ACTIONS.UPDATE_QUANTITY : ACTIONS.ADD_TO_CART;
dispatchCart({ payload: { item, qty }, type });
}
// ... etc
}
If removing, a similar thing happens in handleRemoveFromCart
. If the item's quantity
property is equal to qty
, dispatchCart
is run with type
ACTIONS.REMOVE_FROM_CART
, otherwise it's run with type
ACTIONS.UPDATE_QUANTITY
and the qty
property in the payload
is set to -qty
so that the updateQuantity
function will add the negative amount to the item's quantity, which actually subtracts it.
// Store.js
const Store = () => {
// reducers, other functions...
const handleRemoveFromCart = (item, qty) => {
const removeAll = item.quantity === qty;
removeAll ?
dispatchCart({ type: ACTIONS.REMOVE_FROM_CART, payload: { item } })
:
dispatchCart({ type: ACTIONS.UPDATE_QUANTITY, payload: { qty: -qty, item } });
}
// ... etc
}
// cartHelpers.js
export const updateQuantity = (cart, item, quantity) => (
cart.map(i => (
i.name === item.name ?
{ ...i, quantity: i.quantity += quantity } : i
))
);
The HowManyModal
component is the modal that pops up when an item is clicked. It uses the useState
hook to keep track of the item quantity the user wants to add or remove.
const [howMany, setHowMany] = useState(1);
A form with a number input has a value set to howMany
. howMany
is initialized as 1 so that a quantity of 1 is first displayed in the modal, and the user can adjust from there.
If the modalType
is MODAL.REMOVE
the max number that can be input is the max amount the user has of that item in their cart, otherwise it maxes out at 99.
<input
type="number"
id="how-many"
min="1"
max={`${modalType === MODAL.REMOVE ? itemClicked.quantity : 99}`}
value={howMany}
onChange={handleOnChange}
/>
As mentioned previously, when the "Add to Cart"/"Remove from Cart" button is clicked, handleSubmitItem
runs and dispatches the appropriate reducer based on the modal type. Another function runs next: clearItemClicked
which dispatches dispatchItemClicked
with the type
ACTIONS.CLEAR
. This just sets isModalVisible
back to false and modalType
and item
to null
. Alternatively I could have just pass the dispatch function directly to the modal instead of passing clearItemClicked
down, but I think I did it this way when I was considering separating the itemClicked
from the modal stuff.
That's pretty much the bulk of how it works. The rest of the functions are presentational and broken down to display the items in their containers.
The code can be viewed on github if you'd like to check it out.
Try the demo here
Top comments (2)
About your reducer running twice β I think that's normal, but it shouldn't be a problem because reducers are supposed to be pure functions (with no side effects), which means running it twice should always produce the same result.
It looks like you are accidentally mutating the existing state in
cartHelpers.js
:I think that
+=
should actually just be+
and then you won't have any problems.I also noticed this line in
Store.js
which I think will actually never return true:It looks like it should be comparing
i.name
toitem.name
, although obviously the code works either way since you have written your addToCart as an add / update depending on the current cart. But maybe something to refactor :)ahh good catches, thanks for the second set of eyes! :)