Hi guys,
I am a bit stuck building a prototype app I was experimenting with. I started all good and wrote a bit of code but then realised that when I stop the dev server, it won't run nor will the build build successfully.
The issue started when I introduced a library that uses localStorage and window.CustomEvent.
I managed to fix the gatsby develop script by nulling the library as below in my gatsby-node.js file
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === 'develop-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /@rehooks\/local-storage/,
use: loaders.null()
}
]
}
});
}
};
After doing the above the, I was able to run dev but now builds still fail. The error I got was as below;
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const react_1 = require("react");
> 4 | class LocalStorageChanged extends CustomEvent {
| ^
5 | constructor(payload) {
6 | super(LocalStorageChanged.eventName, { detail: payload });
7 | }
So I also nulled the lib on the build-html stage as well so my gatsby-node.js now looks as below;
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === 'develop-html' || 'build-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /@rehooks\/local-storage/,
use: loaders.null()
}
]
}
});
}
};
Now I start getting a new error in a custom hook file that uses the localStorage lib. The error is as below;
const useCart = () => {
> 6 | const [raw_cart_items] = useLocalStorage(CART_ITEMS);
| ^
7 | const cartItems = JSON.parse(raw_cart_items);
8 |
9 | const addItemToCart = data => {
WebpackError: TypeError: Object(...) is not a function
- useCart.js:6 useCart
The content of the useCart hook is as below;
import { writeStorage, useLocalStorage } from '@rehooks/local-storage';
import { CART_ITEMS } from '../constants/configs';
const useCart = () => {
const [raw_cart_items] = useLocalStorage(CART_ITEMS);
const cartItems = JSON.parse(raw_cart_items);
const addItemToCart = data => {
writeStorage(CART_ITEMS, cartItems ? JSON.stringify([...cartItems, ...data]) : JSON.stringify([...data]));
};
const deleteItemFromCart = product_item_id => {
const newItems = cartItems.filter(item => item.product_item_id !== product_item_id);
writeStorage(CART_ITEMS, JSON.stringify(newItems));
};
const updateCartItem = (products_item_id, data) => {
const newItems = cartItems.map(item => {
if (item.product_item_id === products_item_id) {
item = { ...item, ...data };
return item;
}
return item;
});
writeStorage(CART_ITEMS, JSON.stringify(newItems));
};
const clearCartItems = () => {
writeStorage(CART_ITEMS, JSON.stringify([]));
};
const cartTotalAmount =
cartItems && cartItems.length
? cartItems.reduce((prevAmount, currentItem) => prevAmount + currentItem.amount, 0)
: 0;
return {
addItemToCart,
cartTotalAmount,
cartItems,
deleteItemFromCart,
updateCartItem,
clearCartItems
};
};
export default useCart;
I am using the above hook along with the ContextAPI and I have a container that looks like below;
import React from 'react';
import { SearchContext, CartContext } from '../context/';
import { useSearch, useCart } from '../hooks/';
const AppContainer = ({ children }) => {
const { setSearchValue, toggleSearch, searchText, isSearch } = useSearch(); // search hook
const { addItemToCart, cartItems, deleteItemFromCart, updateCartItem, clearCartItems, cartTotalAmount } = useCart(); // shopping cart hook
return (
<SearchContext.Provider value={{ setSearchValue, toggleSearch, searchText, isSearch }}>
<CartContext.Provider
value={{ addItemToCart, cartItems, deleteItemFromCart, updateCartItem, clearCartItems, cartTotalAmount }}
>
{children}
</CartContext.Provider>
</SearchContext.Provider>
);
};
export default AppContainer;
I use the above container in my gatsby-browser and `gatsby-ssr as depicted below;
System:
OS: macOS 10.14.5
CPU: (4) x64 Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 10.3.0 - ~/.nvm/versions/node/v10.3.0/bin/node
Yarn: 1.7.0 - ~/.yarn/bin/yarn
npm: 6.9.0 - ~/.nvm/versions/node/v10.3.0/bin/npm
Languages:
Python: 2.7.10 - /usr/bin/python
Browsers:
Chrome: 75.0.3770.90
Safari: 12.1.1
npmPackages:
gatsby: ^2.4.3 => 2.4.3
gatsby-cli: ^2.6.7 => 2.6.7
gatsby-image: ^2.0.41 => 2.0.41
gatsby-plugin-manifest: ^2.1.1 => 2.1.1
gatsby-plugin-offline: ^2.1.0 => 2.1.0
gatsby-plugin-prefetch-google-fonts: ^1.4.2 => 1.4.2
gatsby-plugin-react-helmet: ^3.0.12 => 3.0.12
gatsby-plugin-sass: ^2.0.11 => 2.0.11
gatsby-plugin-sharp: ^2.0.37 => 2.0.37
gatsby-source-filesystem: ^2.0.33 => 2.0.33
gatsby-source-graphql: ^2.0.18 => 2.0.19
gatsby-transformer-sharp: ^2.1.19 => 2.1.19
gatsby-config.js: N/A
package.json: N/A
gatsby-node.js:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === 'develop-html' || 'build-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /@rehooks\/local-storage/,
use: loaders.null()
}
]
}
});
}
};
gatsby-browser.js:
import React from 'react';
import { ClientContext } from 'graphql-hooks';
import client from './src/api/graphql-client';
import Layout from './src/components/layout';
import AppContainer from './src/components/app-container';
export const onServiceWorkerUpdateReady = () => {
const answer = window.confirm(`This application has been updated. Reload to display the latest version?`);
if (answer === true) {
window.location.reload();
}
};
export const wrapRootElement = ({ element }) => {
return (
<AppContainer>
<ClientContext.Provider value={client}>{element}</ClientContext.Provider>
</AppContainer>
);
};
export const wrapPageElement = ({ element, props }) => {
return <Layout {...props}>{element}</Layout>;
};
gatsby-ssr.js:
import React from 'react';
import { ClientContext } from 'graphql-hooks';
import AppContainer from './src/components/app-container';
import client from './src/api/graphql-client';
import Layout from './src/components/layout';
export const wrapRootElement = ({ element }) => {
return (
<AppContainer>
<ClientContext.Provider value={client}>{element}</ClientContext.Provider>
</AppContainer>
);
};
export const wrapPageElement = ({ element, props }) => {
return <Layout {...props}>{element}</Layout>;
};
I have a reproduction on Codesandbox on here
The issue here is that you鈥檙e trying to use local storae (useLocalStorage) in render and this isn鈥檛 SSR friendly since local storage is only available in the browser env.
You can fix this by using useLocalStorage in a useEffect hook (these are only run on the client side after render, similar to componentDidMount)
if (stage === 'develop-html' || 'build-html')
That's not how JavaScript comparison works. Replace it with this:
if (stage === 'develop-html' || stage === 'build-html')
Or
if (['develop-html', 'build-html'].includes(stage))
You can fix this by using useLocalStorage in a useEffect hook
:thinking: I don't think you can use any hook inside built-in hooks. That would violate the Rules of Hooks,
馃 I don't think you can use any hook inside built-in hooks. That would violate the Rules of Hooks
You can't conditionally call hooks but you can use hooks inside a hook. That's what most custom hooks do 馃檪
The issue here is that you鈥檙e trying to use local storae (useLocalStorage) in render and this isn鈥檛 SSR friendly since local storage is only available in the browser env.
You can fix this by using useLocalStorage in a useEffect hook (these are only run on the client side after render, similar to componentDidMount)
@sidharthachatterjee do you mind showing how to approach this with an example? Maybe from the sandbox link?
@joeynimu Sure! Found these that illustrate what I meant (I didn't make them, just found them)
You can't conditionally call hooks but you can use hooks inside a hook. That's what most custom hooks do :slightly_smiling_face:
I'm probably commenting about unrelated matter, sorry about that. But, I'm pretty sure you can't call useLocalStorage hook inside useEffect.
function Test() {
useEffect(() => {
useLocalStorage()
})
return <div>test</div>
}
React won't allow it, because useLocalStorage hook is not being called from the Top Level. It's being called inside a callback function.
React won't allow it, because useLocalStorage hook is not being called from the Top Level. It's being called inside a callback function.
Yeah, you're right. What I meant to say is that you could create a custom hook called useLocalStorage that uses useEffect internally and does side effects in it.
React won't allow it, because useLocalStorage hook is not being called from the Top Level. It's being called inside a callback function.Yeah, you're right. What I meant to say is that you could create a custom hook called
useLocalStoragethat usesuseEffectinternally and does side effects in it.
@sidharthachatterjee From the above conversation, I am getting that then your initial approach won't work for I will be calling one custom hook (in this case the @rehooks/local-storage lib) inside useEffect of another custom hook (in this case my useCart CH). Correct me if I am wrong.
Alright, thanks guys for your input.
I ended up fixing this by implementing my own logic in the hook and using ContextAPI to keep my components in sync. I did it like below;
import { useState, useEffect } from 'react';
const PREFIX = `soko_yetu_cart_items`;
const useCart = () => {
const [cartItems, setCartItems] = useState([]);
useEffect(() => {
setCartItems(JSON.parse(window.localStorage.getItem(PREFIX)) || []);
}, [cartItems]);
const addItemToCart = item => {
const items = [...cartItems, ...item];
window.localStorage.setItem(PREFIX, JSON.stringify(items));
setCartItems(items);
};
const deleteItemFromCart = product_item_id => {
const newItems = cartItems.filter(item => item.product_item_id !== product_item_id);
window.localStorage.setItem(PREFIX, newItems);
setCartItems(newItems);
};
const updateCartItem = (products_item_id, data) => {
const newItems = cartItems.map(item => {
if (item.product_item_id === products_item_id) {
item = { ...item, ...data };
return item;
}
return item;
});
window.localStorage.setItem(PREFIX, newItems);
setCartItems(newItems);
};
const clearCartItems = () => {
setCartItems([]);
};
const cartTotalAmount =
cartItems && cartItems.length
? cartItems.reduce((prevAmount, currentItem) => prevAmount + currentItem.amount, 0)
: 0;
return {
addItemToCart,
cartTotalAmount,
cartItems,
deleteItemFromCart,
updateCartItem,
clearCartItems
};
};
export default useCart;
With the above code I also deleted the nulling code in my gatsby-node.js for I am no longer using the library. I hope anyone else with such a problem will get the gist of how to solve this.
I am closing this for now, cheers!