Getting default value from a React Context Consumer in a monorepo Project? How to solve it.
For the last two days, I was wondering, why I'm getting the default value from my react Context.Consumer. everything was going well under development mode, yet when I switch to production mode my context stopped responding in parts of my application, but it was working on other parts of the app. I have tried deferent methods to get the value provided with Context.Provider using useContext and useMemo hooks. but nothing worked as intended. Finally, after many hours of searching and trying out different methods, I have found the solution, and it was a simple one.
To demonstrate the problem, my project hierarchy looked something like this:
/packages/my-ui/build/Basket/index.js:
export default from './Basket';
/packages/my-ui/build/Basket/BasketReducer.js:
const BasketActions = {
ADD: 'add-item',
DEL: 'delete-item',
INC: 'increase-item',
DEC: 'decrease-item',
CLR: 'clear-items',
};
export function AddItemAction(key, item) {
return {
type: BasketActions.ADD,
key,
item,
};
}
export function DelItemAction(key) {
return {
type: BasketActions.DEL,
key,
};
}
export function IncItemAction(key) {
return {
type: BasketActions.INC,
key,
};
}
export function DecItemAction(key) {
return {
type: BasketActions.DEC,
key,
};
}
export function ClrItemsAction() {
return {
type: BasketActions.CLR,
};
}
export default function basketReducer(state = {}, action) {
switch (action.type) {
case BasketActions.ADD: {
const { key, item } = action;
if (state[key]) {
return basketReducer(state, IncItemAction(key));
}
return {
...state,
[key]: {
...item,
count: 1,
},
};
}
case BasketActions.DEL: {
const { key } = action;
const newState = { ...state };
delete newState[key];
return newState;
}
case BasketActions.INC: {
const { key } = action;
const newState = { ...state };
if (newState[key]) {
newState[key].count += 1;
}
return newState;
}
case BasketActions.DEC: {
const { key } = action;
const newState = { ...state };
if (newState[key]) {
newState[key].count -= 1;
}
return newState;
}
case BasketActions.CLR: {
return {};
}
default:
return state;
}
}
/packages/my-ui/build/Basket/BasketContext.js
import React from 'react';
const BasketContext = React.createContext({
getState: () => ({}), // get current state
addItem: () => {}, // add item to basket
delItem: () => {}, // delete item from basket
incItem: () => {}, // increment item's quantity
decItem: () => {}, // decrement item's quantity
clrItems: () => {}, // clear basket
});
export default BasketContext;
/packages/my-ui/build/Basket/BasketContainer.js
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
import BasketContext from './BasketContext';
import basketReducer, {
AddItemAction,
DelItemAction,
IncItemAction,
DecItemAction,
ClrItemsAction,
} from './BasketReducer';
export default function BasketContainer({ children }) {
const [state, dispatch] = useReducer(basketReducer, {});
const service = useMemo(() => ({
getState: () => ({ ...state })
addItem: (key, item) => dispatch(AddItemAction(key, item)),
delItem: (key) => dispatch(DelItemAction(key)),
incItem: (key) => dispatch(IncItemAction(key)),
decItem: (key) => dispatch(DecItemAction(key)),
clrItems: () => dispatch(ClrItemsAction()),
}), [state]);
return (
<BasketContext.Provider value={service}>
{children}
</BasketContext.Provider>
);
}
BasketContainer.propTypes = {
children: PropTypes.node,
};
BasketContainer.defaultProps = {
children: [],
};
/packages/my-ui/build/Basket/Basket.js
import React, { useContext, useMemo } from 'react';
import BasketContext from './BasketContext';
export default function Basket() {
const service = useContext(BasketContext);
const items = useMemo(() => service.getState(), [service]);
const itemKeys = useMemo(() => Object.keys(items), [items]);
const totalPrice = useMemo(() => itemKeys.reduce(
(acc, cur) => (acc + (items[cur].price * items[cur].count)), 0,
), [itemKeys]);
return (
<div>
<div>{itemKeys.map(k => (
<div key={k}>
{items[k].title}-{items[k].price}-{items[k].count}
</div>))}
</div>
<div>Total Price: {totalPrice}</div>
</div>
)
}
/src/client/pages/StorePage..js
import React from 'react';
import Basket from '@myapp/ui/build/Basket';
import BasketContainer from '@myapp/ui/build/Basket/BasketContainer';
import BasketContext from '@myapp/ui/build/Basket/BasketContext';
export default function StorePage() {
const storeItems = [/*my items*/];
return (
<BasketContainer>
<BasketContext.Consumer>
{({addItem}) =>(
<div>
{storeItems.map(i => <button key={i.id} onClick={() =>addItem(i)}>{i.title}</button>)}
</div>
)}
</BasketContext.Consumer>
<div>
<Basket .... /> //items did not show here when invoking addItem
</div>
</BasketContainer>
)
}
To overcome this problem I had to change the way I'm importing my components from 'myapp/ui' package.
/packages/my-ui/build/Basket/index.js:
+import BasketContext from './BasketContext';
+import BasketContainer from './BasketContainer';
export default from './Basket';
+export {
+ BasketContext,
+ BasketContainer,
+}
/src/client/pages/StorePage..js
import React from 'react';
-import Basket from '@myapp/ui/build/Basket';
-import BasketContainer from '@myapp/ui/build/Basket/BasketContainer';
-import BasketContext from '@myapp/ui/build/Basket/BasketContext';
+import Basket, { BasketContainer, BasketContext } from '@myapp/ui/build/Basket';
...