Possibility of implementing Custom React Hook that will instantiate a plain JS class only on the client in Nextjs? - reactjs

Question
Is it possible to implement a Custom React Hook that will instantiate a plain JS class only on the client in Nextjs?
The reason it can only be instantiated in the client is because the plain JS class uses sessionStorage, which is NOT available in Nextjs server.
It seems that a graceful implementation would be one of the following:
Instantiate it inside useEffect since that only runs on the client
Instantiate it inside a Custom Hook so the functionality can be shared across components.
Note:
I do not have a clear mental model of the steps React take with rehydration/render using SSR, so do correct me if my explanation is wrong.
References:
https://tech.willhaben.at/how-to-shoot-yourself-in-the-foot-in-react-401e7dbba720
https://joshwcomeau.com/react/the-perils-of-rehydration/
Classes
CheckoutStorage - plain JS class
Address - component using CheckoutStorage
Instantiate it inside a Custom Hook
// A simple class to retrive data from Session Storage.
class CheckoutStorage {
constructor() {
this._address = JSON.parse(sessionStorage.getItem('selectedAddress'))
// ...other class attributes...
}
get address() {
return this._address
}
// prefer function setters instead of having to do "checkoutStorage = something"
setAddress(address) {
sessionStorage.setItem('selectedAddress', address)
this._address = address
}
// ...other functions...
}
export default CheckoutStorage
Address (Function Component) using Custom Hook
Component that will be using CheckoutStorage
// Custom hook that will return an instantiated CheckoutStorage class on client.
const useCheckoutStorage = () => {
const [checkoutStorage, setCheckoutStorage] = useState(null)
useEffect(() => {
setCheckoutStorage(new CheckoutStorage())
}, [])
return checkoutStorage
}
const Address = () => {
// ..other code..
// Defined at the top of the component so other functions can use it.
const checkoutStorage = useCheckoutStorage()
const [addresses, setAddresses] = useState([])
const [selectedAddressId, setSelectedAddressId] = useState(-1)
useEffect(() => {
// Function that makes API call and calls `setAddresses(..)`
fetchAddresses()
// Get previously selected address ID in session storage
setSelectedAddressId(checkoutStorage.address.id)
}, [])
// other code...
return (
<div>...<div>
)
}
Error
Unable to find 'address' of undefined
This refers to the line:
setSelectedAddressId(checkoutStorage.address.id)
We are using checkoutStorage inside the useEffect()
At this point in time, useCheckoutStorage will return null as it's effect has not been executed.
It will only return the instantiated class CheckoutStorage after setting the state in fetchAdddresses()
Working Version of Address instantiating inside useEffect()
const Address = () => {
// ..other code..
// Previous try with a custom hook
// const checkoutStorage = useCheckoutStorage()
const [checkoutStorage, setCheckoutStorage] = useState(null)
const [addresses, setAddresses] = useState([])
const [selectedAddressId, setSelectedAddressId] = useState(-1)
useEffect(() => {
// Added the immediate 2 lines
const checkoutStorage = new CheckoutStorage()
setCheckoutStorage(checkoutStorage)
// Function that makes API call and calls `setAddresses(..)`
fetchAddresses()
// Get previously selected address ID in session storage
setSelectedAddressId(checkoutStorage.address.id)
}, [])
// other code...
return (
<div>...<div>
)
}
The above implementation works, however, if I would like to use checkoutStorage in another component, I would need to do something similar.
So revisiting the question:
Is it possible to implement a Custom React Hook that will instantiate a plain JS class only on the client in Nextjs?
If not, does this implementation makes sense?
Thanks in advance!
Stay Safe. Stay Home. Stay on StackFlow.

Related

Why "React hook is called in a function" while exporting simple single function that accesses a useAtom state?

I have this simple function that needs to set a state from jotai. I would like this function to have it's own seperate file or be cleaned up somewhere, since it will be reused. I'm new to React and come from Angular. It's kind of a function in a service Angular wise.
How would you solve this properly in React?
Code:
export const setMetamaskWallet = (): void => {
const [, setWeb3] = useAtom(web3Atom);
const [, setLoading] = useAtom(loadingAtom);
const [wallet, setWallet] = useAtom(walletAtom);
setWeb3(
new Web3(window.ethereum),
)
//Todo: Create check if metamask is in browser, otherwise throw error
const setAccount = async () => {
setLoading(true);
const accounts = await window.ethereum.request(
{
method: 'eth_requestAccounts'
},
);
setWallet(
accounts[0],
);
setLoading(false);
}
if (!wallet) {
setAccount();
}
}
Hooks can only be called in React functional components or other hooks. Here it appears you have called it in a typical function, hence the error. You could package this functionality in a custom hook. It may however be most appropriate to keep this function as it is, and instead of calling hooks within it, pass the relevant data into the function as parameters.

React useEffect with Stripe - get client secret asynchronously

I've been plopped into the middle of a react project with no prior knowledge so I'm sure this a duplicate.
I want to use the StripeElements, and the react library is easy enough. I'm just not sure how to get the client secret from the server.
This is what I'm trying:
const stripePromise = loadStripe('stripe_key');
const StripeForm = () => {
const [stripeData, setStripeData] = useState({});
const getClientSecret = async () => {
const { data } = await api.get(`payment/stripe/get-client-secret/`);
return data
}
useEffect(() => {
getClientSecret().then(data => setStripeData(data))
}, [])
if(stripeData.clientSecret) {
return (
<QuoteContainer title={"Pay With Credit Card"}>
<SectionCard>
{stripeData.clientSecret}
</SectionCard>
</QuoteContainer>
);
}
return (<b>Loading</b>)
}
The route payment/stripe/get-client-secret/ returns an array with a key 'clientSecret'. This function is working correctly.
I just can't get the <b>Loading</b> text to be replaced with the QuoteContainer component once the promise is resolved.
If you want to rendering Loading component. You have to set falsy value for stripedData state. Because {} value is truthy. So I think the code is not rendering Loading component. The loading component is not rendered because there is no place to set it as a false value anywhere.
what AJAX library are you using to make the API call? Usually you need to call a function on the response object in order to access the data. For instance, if you are using fetch(), you need to call json() then access the response data.

Use Hooks within class that doesn't doesn't get rendered

I have a child class that returns some JSX (an ion-item) it uses a hook to control some ion-icons (using the hook like a state but only because I can use useEffect which is very handy).
I also have a Bluetooth class. This is home to all the important Bluetooth functions. The reason it is in its own class is because I need this code accessible everywhere in the app (including the list of devices (the ion-items mentioned above) it creates).
I do it like this:
const _Bluetooth = () => {
const [state, setState] = useState([]);
useEffect(() => {
addDevice();
}, [state]);
const devices: any[] = [];
const bluetoothInitialize = () => {
for (let i = 0; i < 5; i++) {
let a = {name: "test", mac: i.toString(), connected:false}
setState({found: [...state.found, a]});
}
}
const connect = (id) => {
console.log("connecting");
}
const addDevice = () => {
let d = state.found[state.found.length - 1]
devices.push(<BluetoothDeviceItem key={d.mac} mac={d.mac} name={d.name} onClick={(id) => connect(id)} connectingState={d.connected ? 'connected' : 'not_connected'}></BluetoothDeviceItem>);
}
return {
devices, bluetoothInitialize, connect
};
}
export default _Bluetooth;
I then create an instance of this class in another file which acts as a global file and then other files import this global file giving access to that one instance of the class:
import _Bluetooth from '../components/bluetooth/Bluetooth'
export const Bluetooth = _Bluetooth();
Unfortunately the _Bluetooth class doesn't work. Since I am using a hook, React expects the component to be rendered and therefore the component needs to return JSX. However I don't want it to return JSX but rather the accessible functions and variables.
Like I said above I am using these hooks more like states but only because of the useEffect function.
I could easily get this working by doing:
const state = {found: []}
and then directly pushing items to the array. This takes away my ability of using useEffect which makes my life a little bit easier but also cleans up the code a little bit.
Is it possible to use hooks without rendering the components / returning any JSX?

React Hook - useCustomHook to set outside useState and useRef

I have the main component along with local state using useState and useRef, I also another custom hook, inside the custom hook I would like to reset my main component's state and ref, am I doing correctly by below?
// custom hook
const useLoadData = ({startLoad, setStartLoad, setLoadCompleted, setUserNameRef}) => {
useEffect(() => {
const fetchUser = async() => { await fetchFromApi(...); return userName;};
if (startLoad) {
const newUserName = fetchUser();
setStartLoad(false);
setLoadCompleted(true);
setUserNameRef(newUserName);
}
}, [startLoad]);
}
// main component
const myMainComp = () {
const [startLoad, setStartLoad] = useState(false);
const [loadCompleted, setLoadCompleted] = useState(false);
const userNameRef = useRef("");
const setUserNameRef = (username) => { this.userNameRef.current = username; }
useLoadData(startLoad, setStartLoad, setLoadCompleted, setUserNameRef);
refreshPage = (userId) => {
setStartLoad(true);
}
}
Am I using the custom hook correctly like by passing all external state value and setState method in? Also I found even I don't use useEffect in my customHook, it also works as expected, so do I need to use useEffect in my custom hook? Any review and suggestion is welcome!
First, I think isn't a good approach you use component methods inside custom hook (like "set" methods provided by useState). You are binding the hook with the main component's internal logic. If the purpose of custom hook is fetch data from API, it need to provide to main component the vars that can be able the main component to manipulate its state by itself (like return isFetching, error, data, etc. and don't call any main component set method inside hook).

React Custom Hooks fetch data globally and share across components?

in this react example from https://reactjs.org/docs/hooks-custom.html, a custom hook is used in 2 different components to fetch online status of a user...
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
then its used in the 2 functions below:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
my question is, will the function be executed individually everywhere where it is imported into a component? Or is there some thing like sharing the state between components, if it is defined as a separate exported function? e.g I execute the function only once, and the "isOnline" state is the same in all components?
And if its individually fetched, how would I have to do it to fetch data only once globally, and then pass it to different components in my React app?
To share state data across multiple components in a large project, I recommend to use Redux or React Context.
Nevertheless, you can implement a global isOnline state using the Observer pattern (https://en.wikipedia.org/wiki/Observer_pattern):
// file: isOnline.tsx
import { useEffect, useState } from 'react';
// use global variables
let isOnline = false;
let observers: React.Dispatch<React.SetStateAction<boolean>>[] = [];
// changes global isOnline state and updates all observers
export const setIsOnline = (online: boolean) => {
isOnline = online;
observers.forEach((update) => update(isOnline));
};
// React Hook
export const useIsOnline = (): [boolean, (online: boolean) => void] => {
const [isOnlineState, setIsOnlineState] = useState<boolean>(isOnline);
useEffect(() => {
// add setIsOnlineState to observers list
observers.push(setIsOnlineState);
// update isOnlineState with latest global isOnline state
setIsOnlineState(isOnline);
// remove this setIsOnlineState from observers, when component unmounts
return () => {
observers = observers.filter((update) => update !== setIsOnlineState);
};
}, []);
// return global isOnline state and setter function
return [isOnlineState, setIsOnline];
};
import { useIsOnline } from './isOnline';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useIsOnline();
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function FriendStatus(props) {
const isOnline = useIsOnline()[0];
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useIsOnline()[0];
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Edit: Inside useIsOnline return isOnlineState created with useState instead of the isOnline variable because otherwise React can't pass the changes down to the component.
Edit 2: Make useEffect independent of isOnlineState so the hook does not unsubscribe and resubscribe on each variable change.
In the case you mention, the function is executed at every component's render. So each component keeps a state value independently from the others. For this specific example it's what I would probably use.
If you need some state data to be shared globally (like authentication status), or between several components at different levels in DOM tree, one option is to use the React context.
First you define a new Context, by using the React.createContext() function.
Check this link for more info: https://reactjs.org/docs/context.html
Then, you must use the Context.Provider (a component which keep context value and manages the updates) at top of your DOM hierarchy and then you can use the hook useContext() to refer to context value (provided from the Context provider) in descendant components at any level.
Check this link for that:
https://reactjs.org/docs/hooks-reference.html#usecontext
You can use this library to convert any custom hook into singleton https://www.npmjs.com/package/react-singleton-hook .
This library creates a wrapper around your custom hook. The original hook is mounted only once into a hidden component. Other components and custom hooks consume wrapper and it delegates calls into your hook.
//useFriendStatusGlobal is a custom hook with globally shared data
const useFriendStatusGlobal = singletonHook(null, useFriendStatus);
Whenever you use a custom hook, there will be separate instances of the hook within your App and they won't share the data unless you are using context API within them which is common across multiple instances or your ChatAPI holds data in one place eg in a singleton class instance or within browserStorage/using API.
useState or useReducers will have separate instances within your App.
You can simply think of this as useState and useEffect being written multiple times within your code app in individual component
As others have mentioned, you need to have a state management in your React app. Context is not recommended for that: It is designed to avoid prop drilling to mid-components, not to be a state management library. All components that consume the context gets re-rendered when it updates, and this behavior is inefficient for a state management.
Redux might be a choice, but sometimes it brings to much boilerplate or concepts.
For this, I would recommend you Recoil, a simple state management library.
In Recoil, you have outsourced your state. Each piece of state is called an Atom. Then you bring the atom and its setState function by using the useRecoilState. Pretty straightforward if you already know how to use hooks.
Give a look at this example:
import React from 'react';
import {
RecoilRoot,
atom,
useRecoilState
} from 'recoil';
const isOnlineState = atom(null);
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
Then, when you try to fetch the friends:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useRecoilState(isOnlineState);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Now you can bring it anywhere:
const [isOnline, setIsOnline] = useRecoilState(isOnlineState)

Resources