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?
Related
I am trying to abstract away my react/tanstack query.
I have a custom hook like the following:
const useGamesApi = () => {
const upcomingGamesQuery = useQuery(
["upcoming", date],
async () => {
const ret = await apiGetUpcomingGames(date);
return ret;
},
{
onSuccess: (data) => {
setGames(data);
},
}
);
return {
games: upcomingGamesQuery,
};
};
export default useGamesApi;
I am trying to consume my API as follows:
const [games, setGames] = useState<Game[]>([]);
const gamesApi = useGamesApi();
useEffect(() => {
setGames(gamesApi.games.data);
}, []);
This leads to compilation errors and also the value of my games state variable remains an empty array, as if the useEffect never ran.
Basically I am trying to abstract away my react query to provide a simplified way of interacting with it for my components, whilst also giving it a chance to modify the parameter of the date, so that I can be able to set until which date I would like to query.
What would be the correct (compilation vise) and idiomatic way of doing this with react?
(note I am using this in a react native project, not sure if it counts.)
As per rules , You need to add all the variables used inside useEffect as dependency so that it reacts once the value is changed.
You don't really need useEffect for you scenario. It is used to cause side effects. simply do it like :
const games: Game[] = gamesApi?.games?.data;
const games: Game[] = gamesApi?.games?.data || []; // incase you need default value
I have a class in a separate file and two or more different react components that need to use the class methods
One approach was I initially created an instance of the class outside the react components to prevent re rendering and having re-initialize the class
const utilityClass = new UtilityClass()
function ReactComponent() {
const doSomething = () => {
return utilityClass.doingSomething()
}
}
but then for the second react component in a different file I will have to do the same thing right like below
const utilityClass = new UtilityClass()
function SecondReactComponent() {
const doSomething = () => {
return utilityClass.doingSomething()
}
}
Even though it wont re-initialize on component re-render I am still creating an instance of the utility class multiple times across the different react components so I tried useMemo which also worked like below:
function SecondReactComponent() {
const utilityClass = useMemo(() => new utilityClass(), []);
const doSomething = () => {
return utilityClass.doingSomething()
}
}
And I am wondering which is the best approach because I also tried useCallback and for some reason that did not work and will appreciate if someone gave me more insights on the best practice to do this thanks
Just instantiate the class and export it at the top level of one of your modules. For example:
./First.jsx:
// class UtilityClass {/* ...*/}
export const utilityClass = new UtilityClass();
export function ReactComponent () {
const doSomething = () => {
return utilityClass.doingSomething();
};
}
Then, in every other module where you want to use it (in other components, etc.), just import and use it:
./Second.jsx:
import {utilityClass} from './First';
export function SecondReactComponent () {
const doSomething = () => {
return utilityClass.doingSomething();
};
}
I am new to using React and Recoil and want to display real-time charts (using D3) from data that is gathered in real-time using the Web Bluetooth API.
In a nutshell, after calling await myCharacteristic.startNotifications() and myCharacteristic.addEventListener('characteristicvaluechanged', handleNotifications), the handleNotifications callback is called each time a new value is notified from a Bluetooth device (see this example).
I am using hooks and tried to modify a recoil state from the callback (this was simplified to the extreme, I hope it is representative):
export const temperatureState = atom({
key: 'temperature',
default: 0
})
export function BluetoothControls() {
const setTemperature = useSetRecoilState(temperatureState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperature(event.target.value.getInt16(0))
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This work fine if I want to display the latest value somewhere in the app. However, I am interested in keeping the last few (let's say 10) values in a circular buffer to draw a D3 chart.
I tried something along the lines of:
export const temperatureListState = atom({
key: 'temperature-list',
default: []
})
export function BluetoothControls() {
const [temperatureList, setTemperatureList] = useRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
let temperatureListCopy = temperatureList.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
setTemperatureList(temperatureListCopy)
}
}
return <button onClick={nofity}/>Start notifications</button>
}
However, is is pretty clear that I am running into the issue described here where the function is using an old version of temperatureList that is captured during render. As a result, temperatureState is always empty and then replaced with a list of one element.
How to maintain a consistent list in a React state/Recoil atom that is updated from an external callback? I think this issue is a bit similar but I'd like to avoid using another extension like Recoil Nexus.
useSetRecoilState accepts an updater function as an argument with the value to be updated as the first parameter:
export function BluetoothControls() {
const setTemperatureList = useSetRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperatureList(t => {
let temperatureListCopy = t.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
return temperatureListCopy
})
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This solves the issue as the updater function is only evaluated on events.
It seems a lot of my custom React Hooks don't work well, or seem to cause a big performance overhead if they are reused in multiple places. For example:
A hook that is only called in the context provider and sets up some context state/setters for the rest of the app to use
A hook that should only be called in a root component of a Route to setup some default state for the page
A hook that checks if a resource is cached and if not, retrieves it from the backend
Is there any way to ensure that a hook is only referenced once in a stack? Eg. I would like to trigger a warning or error when I call this hook in multiple components in the same cycle.
Alternatively, is there a pattern that I should use that simply prevents it being a problem to reuse such hooks?
Example of hook that should not be reused (third example). If I would use this hook in multiple places, I would most likely end up making unnecessary API calls.
export function useFetchIfNotCached({id}) {
const {apiResources} = useContext(AppContext);
useEffect(() => {
if (!apiResources[id]) {
fetchApiResource(id); // sets result into apiResources
}
}, [apiResources]);
return apiResources[id];
}
Example of what I want to prevent (please don't point out that this is a contrived example, I know, it's just to illustrate the problem):
export function Parent({id}) {
const resource = useFetchIfNotCached({id});
return <Child id={id}>{resource.Name}</Child>
}
export function Child({id}) {
const resource = useFetchIfNotCached({id}); // <--- should not be allowed
return <div>Child: {resource.Name}</div>
}
You need to transform your custom hooks into singleton stores, and subscribe to them directly from any component.
See reusable library implementation.
const Comp1 = () => {
const something = useCounter(); // is a singleton
}
const Comp2 = () => {
const something = useCounter(); // same something, no reset
}
To ensure that a hook called only once, you only need to add a state for it.
const useCustomHook = () => {
const [isCalled, setIsCalled] = useState(false);
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
setIsCalled(true);
setState(value);
};
return { state, setState: onSetState, isCalled };
};
Edit:
If you introduce a global variable in your custom hook you will get the expected result. Thats because global variables are not tied to component's lifecycle
let isCalledOnce = false;
const useCustomHook = () => {
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
if (!isCalledOnce) {
isCalledOnce = true;
setState(false);
}
};
return { state, setState: onSetState, isCalled };
};
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.